From 3edf6ff3518384d2490ac388cde10969e49f44ee Mon Sep 17 00:00:00 2001 From: Pouya Defaei Date: Sat, 2 May 2026 08:52:08 +0330 Subject: [PATCH] add readme for documentation --- README.md | 57 ++-- ecosystem.config.js | 17 ++ src/app/[locale]/academy/[slug]/page.tsx | 43 ++- src/app/[locale]/academy/page.tsx | 26 +- src/app/[locale]/network/page.tsx | 14 +- src/app/[locale]/software/page.tsx | 4 +- src/components/academy/AcademyClient.tsx | 178 ++++++++++++ src/components/academy/AcademyHeader.tsx | 58 ---- src/components/academy/ArticleGrid.tsx | 126 --------- src/components/academy/FeaturedArticle.tsx | 52 ---- src/components/network/Hero.tsx | 35 ++- src/components/network/Projects.tsx | 163 ++++------- src/components/network/Services.tsx | 34 ++- src/components/network/Technologies.tsx | 12 +- src/components/shared/footer/Footer.tsx | 2 +- src/components/single-academy/ArticleBody.tsx | 255 +++++++++--------- .../single-academy/ArticleHeader.tsx | 110 +++++--- .../single-academy/RelatedArticles.tsx | 76 +++--- src/components/software/Projects.tsx | 12 +- src/components/software/{ => hero}/Hero.tsx | 17 +- src/components/software/hero/HeroButtons.tsx | 30 +++ src/messages/ar.json | 68 +++++ src/messages/en.json | 68 +++++ src/messages/fa.json | 68 +++++ src/utilities/lib/calculate-reading-time.ts | 15 ++ src/utilities/lib/format-date-by-locale.ts | 2 +- src/utilities/types/blog.type.ts | 49 +++- 27 files changed, 927 insertions(+), 664 deletions(-) create mode 100644 ecosystem.config.js create mode 100644 src/components/academy/AcademyClient.tsx delete mode 100644 src/components/academy/AcademyHeader.tsx delete mode 100644 src/components/academy/ArticleGrid.tsx delete mode 100644 src/components/academy/FeaturedArticle.tsx rename src/components/software/{ => hero}/Hero.tsx (67%) create mode 100644 src/components/software/hero/HeroButtons.tsx create mode 100644 src/utilities/lib/calculate-reading-time.ts diff --git a/README.md b/README.md index e215bc4..c07f777 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,51 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +# 🚀 Robin Nework New Client Setup -## Getting Started +Follow these steps to run the frontend locally. -First, run the development server: +--- + +## 1️⃣ Install Dependencies ```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev +npm install ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +--- -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +## 2️⃣ Create `.env` File -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +Create a `.env` file in the root directory and add: -## Learn More +```env +NEXT_PUBLIC_BACKEND_URL=api.robinnetwork.ir (for example) +NEXT_PUBLIC_BACKEND_URL_LOCAL=http://127.0.0.1:4000 +``` -To learn more about Next.js, take a look at the following resources: +⚠️ Make sure your backend server is running on port `4000`. -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +--- -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## 3️⃣ Run the Application -## Deploy on Vercel +```bash +npm run build +npm start +``` -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. +--- -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. +## ✅ Application URL + +After running the command, the app will be available at: + +``` +http://localhost:3000 +``` + +--- + +## 📝 Notes + +- Restart the server after changing environment variables. +- Ensure the backend is running before testing API requests. +- `NEXT_PUBLIC_` variables are exposed to the browser. diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..c8fb2ed --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,17 @@ +module.exports = { + apps: [ + { + name: "new_client", + script: "npm", + args: "start", + cwd: __dirname, + instances: 1, + exec_mode: "fork", + watch: false, + env: { + NEXT_PUBLIC_BACKEND_URL: "http://127.0.0.1:4000", + NEXT_PUBLIC_BACKEND_URL_LOCAL: "http://127.0.0.1:4000", + }, + }, + ], +}; diff --git a/src/app/[locale]/academy/[slug]/page.tsx b/src/app/[locale]/academy/[slug]/page.tsx index cab8bd8..3a31f14 100644 --- a/src/app/[locale]/academy/[slug]/page.tsx +++ b/src/app/[locale]/academy/[slug]/page.tsx @@ -1,13 +1,35 @@ -import Consultation from "@/components/network/Consultation"; -import ContactFooter from "@/components/network/ContactFooter"; import ArticleBody from "@/components/single-academy/ArticleBody"; import ArticleHeader from "@/components/single-academy/ArticleHeader"; import RelatedArticles from "@/components/single-academy/RelatedArticles"; +import { BACKEND_URL_LOCAL } from "@/utilities/constants/urls.constant"; +import { calculateReadingTime } from "@/utilities/lib/calculate-reading-time"; +import { Blog, BlogTranslation } from "@/utilities/types/blog.type"; +import { notFound } from "next/navigation"; + +export default async function SingleAcademyPage({ params }: { params: Promise<{ slug: string; locale: string }> }) { + const { slug, locale } = await params; + + const [blog, relatedBlogs]: [Blog | null, Blog[]] = await Promise.all([ + fetch(`${BACKEND_URL_LOCAL}/blogs/single/${slug}/${locale}`) + .then((res) => res.json()) + .then((res) => res.data) + .catch(() => null), + fetch(`${BACKEND_URL_LOCAL}/blogs/related/${slug}/${locale}`) + .then((res) => res.json()) + .then((res) => res.data) + .catch(() => null), + ]); + + if (!blog) { + return notFound(); + } + + const translation: BlogTranslation = blog.translations.find((t) => t.language === locale) || blog.translations[0]; + const isRtl = locale === "fa" || locale === "ar"; + const readingTime = calculateReadingTime(translation); -export default function SingleAcademyPage() { return ( -
- {/* Background Grid Pattern */} +
- - - + + +
- -
- - -
); } diff --git a/src/app/[locale]/academy/page.tsx b/src/app/[locale]/academy/page.tsx index c329d94..1839fac 100644 --- a/src/app/[locale]/academy/page.tsx +++ b/src/app/[locale]/academy/page.tsx @@ -1,21 +1,19 @@ -import AcademyHeader from "@/components/academy/AcademyHeader"; -import ArticleGrid from "@/components/academy/ArticleGrid"; -import FeaturedArticle from "@/components/academy/FeaturedArticle"; -import Consultation from "@/components/network/Consultation"; -import ContactFooter from "@/components/network/ContactFooter"; +import AcademyClient from "@/components/academy/AcademyClient"; +import { BACKEND_URL_LOCAL } from "@/utilities/constants/urls.constant"; +import { Blog } from "@/utilities/types/blog.type"; + +export default async function AcademyPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + + const data: Blog[] = await fetch(`${BACKEND_URL_LOCAL}/blogs/all/${locale}`) + .then((res) => res.json()) + .then((res) => res.data) + .catch(() => []); -export default function AcademyPage() { return (
- - - -
- -
- - +
); diff --git a/src/app/[locale]/network/page.tsx b/src/app/[locale]/network/page.tsx index 61bbb60..bc6909c 100644 --- a/src/app/[locale]/network/page.tsx +++ b/src/app/[locale]/network/page.tsx @@ -4,8 +4,16 @@ import Technologies from "@/components/network/Technologies"; import Projects from "@/components/network/Projects"; import Consultation from "@/components/network/Consultation"; import ContactFooter from "@/components/network/ContactFooter"; +import { Portfolio } from "@/utilities/types/portfolio.type"; +import { BACKEND_URL_LOCAL } from "@/utilities/constants/urls.constant"; + +export default async function NetworkPage({ params }: { params: Promise<{ locale: string }> }) { + const { locale } = await params; + + const latestPortfolios: Portfolio[] = await fetch(`${BACKEND_URL_LOCAL}/portfolios/category/network/${locale}`) + .then((res) => res.json()) + .then((res) => res.data); -export default function NetworkPage() { return (
@@ -14,9 +22,7 @@ export default function NetworkPage() { - - - +
); } diff --git a/src/app/[locale]/software/page.tsx b/src/app/[locale]/software/page.tsx index 81a62eb..f80a1b8 100644 --- a/src/app/[locale]/software/page.tsx +++ b/src/app/[locale]/software/page.tsx @@ -1,4 +1,4 @@ -import Hero from "@/components/software/Hero"; +import Hero from "@/components/software/hero/Hero"; import Services from "@/components/software/Services"; import TechStack from "@/components/software/TechStack"; import Process from "@/components/software/Process"; @@ -9,7 +9,7 @@ import { Portfolio } from "@/utilities/types/portfolio.type"; export default async function SoftwarePage({ params }: { params: Promise<{ locale: string }> }) { const { locale } = await params; - const latestPortfolios: Portfolio[] = await fetch(`${BACKEND_URL_LOCAL}/portfolios/category/network/${locale}`) + const latestPortfolios: Portfolio[] = await fetch(`${BACKEND_URL_LOCAL}/portfolios/category/software/${locale}`) .then((res) => res.json()) .then((res) => res.data); diff --git a/src/components/academy/AcademyClient.tsx b/src/components/academy/AcademyClient.tsx new file mode 100644 index 0000000..71f06ce --- /dev/null +++ b/src/components/academy/AcademyClient.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { Blog } from "@/utilities/types/blog.type"; +import { useState, useMemo } from "react"; +import { useLocale, useTranslations } from "next-intl"; +import { calculateReadingTime } from "@/utilities/lib/calculate-reading-time"; +import { formatDateByLocale } from "@/utilities/lib/format-date-by-locale"; +import Image from "next/image"; +import { BACKEND_URL } from "@/utilities/constants/urls.constant"; +import Link from "next/link"; + +export default function AcademyClient({ data }: { data: Blog[] }) { + const t = useTranslations("academy.page"); + const locale = useLocale(); + const [search, setSearch] = useState(""); + + const filteredData = useMemo(() => { + if (!data) return []; + + return data + .filter((blog) => { + if (!search) return true; + const translation = blog.translations?.[0]; + if (!translation) return false; + + const searchLower = search.toLowerCase(); + return translation.title.toLowerCase().includes(searchLower) || translation.description.toLowerCase().includes(searchLower); + }) + .slice(0, 6); + }, [data, search]); + + const featuredPost = filteredData[0]; + const gridPosts = filteredData.slice(1); + + return ( + <> +
+
+ + + + {t("hero.badge")} +
+ +

+ {t("hero.title1")} + {t("hero.title2")} +

+ +

{t("hero.subtitle")}

+ +
+ setSearch(target.value)} + className="w-full px-6 py-4 text-sm transition border bg-panel border-border rounded-xl focus:outline-none focus:border-accent" + /> + + + +
+
+ + {featuredPost && ( + +
+
+ + + + + {formatDateByLocale(featuredPost.publishedAt, locale)} + + + + + + {calculateReadingTime(featuredPost.translations[0])} {t("minutesRead")} + +
+

+ {featuredPost.translations?.[0]?.title} +

+

{featuredPost.translations?.[0]?.description}

+
+ +
+ {featuredPost.translations[0].title} +
+ + )} + + {gridPosts.length > 0 && ( +
+
+ {gridPosts.map((b) => { + const translation = b.translations?.[0]; + return ( + +
+ {translation.title} +
+
+
+ + + + + {formatDateByLocale(b.publishedAt, locale)} + + + + + + {calculateReadingTime(translation)} {t("minutesRead")} + +
+ +

{translation?.title}

+ +

{translation?.description}

+
+ + ); + })} +
+ + {/* Pagination */} + {/*
+ + + +
*/} +
+ )} + + ); +} diff --git a/src/components/academy/AcademyHeader.tsx b/src/components/academy/AcademyHeader.tsx deleted file mode 100644 index 663e557..0000000 --- a/src/components/academy/AcademyHeader.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { useState } from "react"; - -export default function AcademyHeader() { - const [activeFilter, setActiveFilter] = useState("همه مقالات"); - const filters = ["همه مقالات", "شبکه", "امنیت", "برنامه‌نویسی", "DevOps"]; - - return ( -
-
- - - - آکادمی رابین شبکه -
- -

- دانش و تجربه - به اشتراک گذاشته شده -

- -

مقالات تخصصی، آموزش‌ها و تجربیات واقعی از پروژه‌های شبکه و نرم‌افزار

- -
- - - - -
- -
- {filters.map((filter) => ( - - ))} -
-
- ); -} diff --git a/src/components/academy/ArticleGrid.tsx b/src/components/academy/ArticleGrid.tsx deleted file mode 100644 index 09d8cff..0000000 --- a/src/components/academy/ArticleGrid.tsx +++ /dev/null @@ -1,126 +0,0 @@ -export default function ArticleGrid() { - const articles = [ - { - title: "طراحی شبکه WiFi برای هتل ۱۰۰ اتاقه", - desc: "انتخاب تجهیزات، Site Survey و پیکربندی UniFi برای پوشش کامل و بدون dead zone.", - category: "شبکه", - date: "۱ هفته پیش", - readTime: "۱۸ دقیقه", - tags: ["WiFi", "UniFi"], - gradient: "from-[#1a2332] to-[#121418]", - }, - { - title: "معماری Microservices: از تئوری تا عمل", - desc: "چگونه یک سیستم Monolithic را به معماری Microservices تبدیل کنیم؟ تجربه واقعی از یک پروژه.", - category: "برنامه‌نویسی", - date: "۱ هفته پیش", - readTime: "۲۰ دقیقه", - tags: ["Microservices", "Architecture"], - gradient: "from-[#231a32] to-[#121418]", - }, - { - title: "۱۰ نکته طلایی برای امن‌سازی Firewall", - desc: "بهترین روش‌ها و تکنیک‌های پیکربندی فایروال برای محافظت حداکثری از شبکه سازمانی.", - category: "امنیت", - date: "۵ روز پیش", - readTime: "۱۲ دقیقه", - tags: ["Firewall", "Security"], - gradient: "from-[#321a1a] to-[#121418]", - }, - { - title: "VPN چیست و چگونه کار می‌کند؟", - desc: "آشنایی با انواع VPN، پروتکل‌ها و نحوه پیاده‌سازی Site-to-Site VPN با MikroTik.", - category: "شبکه", - date: "۲ هفته پیش", - readTime: "۱۰ دقیقه", - tags: ["VPN", "IPSec"], - gradient: "from-[#1a2b32] to-[#121418]", - }, - { - title: "راه‌اندازی CI/CD Pipeline با GitHub Actions", - desc: "پیاده‌سازی کامل یک pipeline برای deploy خودکار اپلیکیشن Next.js روی VPS.", - category: "DevOps", - date: "۲ هفته پیش", - readTime: "۲۵ دقیقه", - tags: ["CI/CD", "GitHub"], - gradient: "from-[#2e321a] to-[#121418]", - }, - ]; - - return ( -
-
- {articles.map((article, i) => ( -
- {/* تصویر کاور کارت */} -
-
- {article.category} -
-
- - {/* محتوای کارت */} -
-
- - - - - {article.date} - - - - - - {article.readTime} - -
- -

{article.title}

- -

{article.desc}

- -
- {article.tags.map((tag) => ( - - {tag} - - ))} -
-
-
- ))} -
- - {/* Pagination (صفحه‌بندی) */} -
- - - - - - -
-
- ); -} diff --git a/src/components/academy/FeaturedArticle.tsx b/src/components/academy/FeaturedArticle.tsx deleted file mode 100644 index 11ee0ac..0000000 --- a/src/components/academy/FeaturedArticle.tsx +++ /dev/null @@ -1,52 +0,0 @@ -export default function FeaturedArticle() { - return ( -
-
-
- - - - - ۲ روز پیش - - - - - - ۱۵ دقیقه - -
- -

- راهنمای کامل پیکربندی MikroTik برای شبکه‌های سازمانی -

- -

- در این مقاله به صورت جامع نحوه پیکربندی روتر MikroTik برای یک شبکه سازمانی با ۱۰۰+ کاربر را بررسی می‌کنیم. از VLAN Segmentation تا QoS و - Firewall Rules. -

- -
- {["MikroTik", "VLAN", "Routing"].map((tag) => ( - - {tag} - - ))} -
-
- - {/* بخش تصویر کاور (از گرادیانت به عنوان جایگزین تصویر استفاده شده) */} -
-
شبکه
-
-
- ); -} diff --git a/src/components/network/Hero.tsx b/src/components/network/Hero.tsx index 556211e..3f1eefb 100644 --- a/src/components/network/Hero.tsx +++ b/src/components/network/Hero.tsx @@ -1,8 +1,14 @@ "use client"; import { useState, useEffect } from "react"; +import { useTranslations, useLocale } from "next-intl"; +import { handleScrollToId } from "@/utilities/lib/scroll"; export default function Hero() { + const t = useTranslations("network.hero"); + const locale = useLocale(); + const isRTL = locale === "fa" || locale === "ar"; + const [activeIndicators, setActiveIndicators] = useState({ sys: true, act: false, pwr: true }); const [activeSfp, setActiveSfp] = useState([true, false]); const [activeRj45, setActiveRj45] = useState(Array(24).fill(false)); @@ -22,18 +28,19 @@ export default function Hero() { return (
- مهندسی شبکه و امنیت + {t("badge")}

- زیرساخت شبکه پایدار و امن + {t("title1")} {t("title2")}

-

- طراحی، پیاده‌سازی و مدیریت زیرساخت‌های شبکه پیچیده با تمرکز بر امنیت، پایداری و مقیاس‌پذیری -

+

{t("subtitle")}

- {/* روتر با max-width برای محدود کردن سایز در دسکتاپ و overflow-x-auto برای موبایل */} -
+ {/* Hardware Interface - Forced LTR to keep technical layout consistent in all languages */} +
+CRS326-24G-2S
@@ -97,11 +104,17 @@ export default function Hero() {
- -
diff --git a/src/components/network/Projects.tsx b/src/components/network/Projects.tsx index ca20ba9..94aad6d 100644 --- a/src/components/network/Projects.tsx +++ b/src/components/network/Projects.tsx @@ -1,141 +1,75 @@ "use client"; import { useRef, useState } from "react"; +import { useTranslations, useLocale } from "next-intl"; +import { Portfolio } from "@/utilities/types/portfolio.type"; -export default function Projects() { - const projects = [ - { - category: "شبکه بی‌سیم", - client: "هتل پارسیان", - title: "WiFi سراسری هتل", - desc: "پوشش کامل WiFi با امنیت بالا و تفکیک شبکه مهمان", - tag: "Wireless 📡", - year: "۲۰۲۵", - }, - { - category: "امنیتی", - client: "بانک سامان", - title: "پیاده‌سازی فایروال", - desc: "نصب و پیکربندی تجهیزات Fortinet جهت ارتقای امنیت شبکه", - tag: "Security 🛡️", - year: "۲۰۲۴", - }, - { - category: "سازمانی", - client: "شرکت مپنا", - title: "زیرساخت دیتاسنتر", - desc: "طراحی و اجرای پسیو و اکتیو شبکه دیتاسنتر مرکزی", - tag: "Data Center 🏢", - year: "۲۰۲۴", - }, - { - category: "ارتباطات", - client: "بیمه پاسارگاد", - title: "ارتباط شعب", - desc: "برقراری ارتباط امن بین شعب از طریق تونل‌های IPsec", - tag: "VPN 🔗", - year: "۲۰۲۳", - }, - { - category: "امنیتی", - client: "بانک سامان", - title: "پیاده‌سازی فایروال", - desc: "نصب و پیکربندی تجهیزات Fortinet جهت ارتقای امنیت شبکه", - tag: "Security 🛡️", - year: "۲۰۲۴", - }, - { - category: "سازمانی", - client: "شرکت مپنا", - title: "زیرساخت دیتاسنتر", - desc: "طراحی و اجرای پسیو و اکتیو شبکه دیتاسنتر مرکزی", - tag: "Data Center 🏢", - year: "۲۰۲۴", - }, - { - category: "ارتباطات", - client: "بیمه پاسارگاد", - title: "ارتباط شعب", - desc: "برقراری ارتباط امن بین شعب از طریق تونل‌های IPsec", - tag: "VPN 🔗", - year: "۲۰۲۳", - }, - ]; +export default function Projects({ data }: { data: Portfolio[] }) { + const t = useTranslations("network.projects"); + const locale = useLocale(); + const isRtl = locale === "fa" || locale === "ar"; - const sliderRef = useRef(null); + const sliderRef = useRef(null); const [isDragging, setIsDragging] = useState(false); const [startX, setStartX] = useState(0); const [scrollLeft, setScrollLeft] = useState(0); + const scrollAmount = 320; + const nextSlide = () => { if (sliderRef.current) { - sliderRef.current.scrollBy({ left: -320, behavior: "smooth" }); + sliderRef.current.scrollBy({ left: isRtl ? -scrollAmount : scrollAmount, behavior: "smooth" }); } }; const prevSlide = () => { if (sliderRef.current) { - sliderRef.current.scrollBy({ left: 320, behavior: "smooth" }); + sliderRef.current.scrollBy({ left: isRtl ? scrollAmount : -scrollAmount, behavior: "smooth" }); } }; - // Mouse drag handlers for desktop - const handleMouseDown = (e: any) => { + const handleMouseDown = (e: React.MouseEvent) => { setIsDragging(true); - setStartX(e.pageX - sliderRef.current.offsetLeft); - setScrollLeft(sliderRef.current.scrollLeft); + if (sliderRef.current) { + setStartX(e.pageX - sliderRef.current.offsetLeft); + setScrollLeft(sliderRef.current.scrollLeft); + } }; - const handleMouseLeave = () => { - setIsDragging(false); - }; + const handleMouseLeave = () => setIsDragging(false); + const handleMouseUp = () => setIsDragging(false); - const handleMouseUp = () => { - setIsDragging(false); - }; - - const handleMouseMove = (e: any) => { - if (!isDragging) return; + const handleMouseMove = (e: React.MouseEvent) => { + if (!isDragging || !sliderRef.current) return; e.preventDefault(); const x = e.pageX - sliderRef.current.offsetLeft; - const walk = (x - startX) * 2; // Scroll speed multiplier + const walk = (x - startX) * 2; sliderRef.current.scrollLeft = scrollLeft - walk; }; - return ( -
-
-
-

پروژه‌های شبکه

-

نمونه‌ای از پروژه‌های شبکه اجرا شده

-
+ const getYear = (date: Date | string) => { + return new Date(date).toLocaleDateString(locale, { year: "numeric" }); + }; -
-
- {["همه", "سازمانی", "امنیتی", "بی‌سیم"].map((tab, i) => ( - - ))} + return ( +
+
+
+
+

{t("title")}

+

{t("subtitle")}

+
-
- {/* Slider Container with Drag Events */} + {/* Slider Container */}
- {projects.map((proj, i) => ( + {data.map((p) => (
-
+
+
- {proj.category} - {proj.client} + {t(`categories.${p.category}`)} + {p.employer}
-

{proj.title}

-

{proj.desc}

-
- {proj.tag} - {proj.year} + +

{p.title}

+

{p.description}

+ +
+ {getYear(new Date(p.createdAt))}
))} diff --git a/src/components/network/Services.tsx b/src/components/network/Services.tsx index 8b5f3f1..0cf4775 100644 --- a/src/components/network/Services.tsx +++ b/src/components/network/Services.tsx @@ -1,29 +1,35 @@ -export default function ServicesSection() { - const services = [ - { title: "شبکه بی‌سیم", subtitle: "WIFI Enterprise", icon: "📶" }, - { title: "امنیت", subtitle: "IDS/IPS و Firewall", icon: "🛡️" }, - { title: "طراحی شبکه", subtitle: "معماری و توپولوژی", icon: "📐" }, - { title: "پشتیبانی", subtitle: "Support ۲۴/۷", icon: "🎧" }, - { title: "VPN & WAN", subtitle: "اتصال بین سایت", icon: "🔗" }, - { title: "مانیتورینگ", subtitle: "نظارت ۲۴/۷", icon: "📈" }, +"use client"; + +import { useTranslations } from "next-intl"; + +export default function Services() { + const t = useTranslations("network.services"); + + const serviceItems = [ + { key: "wifi", icon: "📶" }, + { key: "security", icon: "🛡️" }, + { key: "design", icon: "📐" }, + { key: "support", icon: "🎧" }, + { key: "vpn", icon: "🔗" }, + { key: "monitoring", icon: "📈" }, ]; return (
-
-

خدمات شبکه

-

راهکارهای جامع برای زیرساخت شبکه سازمان شما

+
+

{t("sectionTitle")}

+

{t("sectionSubtitle")}

- {services.map((svc, i) => ( + {serviceItems.map((svc, i) => (
{svc.icon}
-

{svc.title}

-

{svc.subtitle}

+

{t(`items.${svc.key}.title`)}

+

{t(`items.${svc.key}.subtitle`)}

))}
diff --git a/src/components/network/Technologies.tsx b/src/components/network/Technologies.tsx index 46bb075..3084a9c 100644 --- a/src/components/network/Technologies.tsx +++ b/src/components/network/Technologies.tsx @@ -1,4 +1,10 @@ +"use client"; + +import { useTranslations } from "next-intl"; + export default function Technologies() { + const t = useTranslations("network.technologies"); + const techs = [ { name: "VPN", @@ -53,9 +59,9 @@ export default function Technologies() { return (
-
-

تکنولوژی‌ها

-

تجهیزات و فناوری‌های مورد استفاده

+
+

{t("title")}

+

{t("subtitle")}

diff --git a/src/components/shared/footer/Footer.tsx b/src/components/shared/footer/Footer.tsx index dc0fc49..68b45b6 100644 --- a/src/components/shared/footer/Footer.tsx +++ b/src/components/shared/footer/Footer.tsx @@ -8,7 +8,7 @@ export default function Footer() { const isRtl = locale === "fa" || locale === "ar"; return ( -
+
diff --git a/src/components/single-academy/ArticleBody.tsx b/src/components/single-academy/ArticleBody.tsx index b95752e..0f5e6f9 100644 --- a/src/components/single-academy/ArticleBody.tsx +++ b/src/components/single-academy/ArticleBody.tsx @@ -1,55 +1,95 @@ -import React from "react"; -import { List, BarChart2, Tag, Lightbulb, CheckCircle2, Share2, Copy } from "lucide-react"; +"use client"; + +import { useEffect, useState } from "react"; +import { List, BarChart2, Share2, Copy, Check } from "lucide-react"; +import { Blog, BlogTranslation, HeadingBlock } from "@/utilities/types/blog.type"; +import Image from "next/image"; +import { BACKEND_URL } from "@/utilities/constants/urls.constant"; +import { useTranslations } from "next-intl"; +import { handleScrollToId } from "@/utilities/lib/scroll"; + +interface Props { + blog: Blog; + translation: BlogTranslation; +} + +export default function ArticleBody({ blog, translation }: Props) { + const t = useTranslations("academy.single"); + const [readingProgress, setReadingProgress] = useState(0); + const [isCopied, setIsCopied] = useState(false); + + useEffect(() => { + const handleScroll = () => { + const scrollY = window.scrollY; + const docHeight = document.documentElement.scrollHeight; + const winHeight = window.innerHeight; + const scrollPercent = scrollY / (docHeight - winHeight); + setReadingProgress(Math.min(100, Math.max(0, scrollPercent * 100))); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + setIsCopied(true); + setTimeout(() => setIsCopied(false), 2000); + } catch (err) { + null; + } + }; + + const handleShare = async () => { + if (navigator.share) { + try { + await navigator.share({ + title: translation.title, + url: window.location.href, + }); + } catch (err) { + null; + } + } else { + handleCopy(); + } + }; + + const headings = translation.editor.filter((block): block is HeadingBlock => block.type === "heading"); -export default function ArticleBody() { return (
- {/* Sidebar */}
); } diff --git a/src/components/software/hero/HeroButtons.tsx b/src/components/software/hero/HeroButtons.tsx new file mode 100644 index 0000000..f75bf9e --- /dev/null +++ b/src/components/software/hero/HeroButtons.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { handleScrollToId } from "@/utilities/lib/scroll"; +import { ArrowUpIcon, ChevronLeft, ChevronRight } from "lucide-react"; +import { useLocale, useTranslations } from "next-intl"; + +export default function HeroButtons() { + const t = useTranslations("software.hero"); + const locale = useLocale(); + const isRtl = locale === "fa" || locale === "ar"; + + const ArrowIcon = isRtl ? ChevronLeft : ChevronRight; + + return ( +
+ + +
+ ); +} diff --git a/src/messages/ar.json b/src/messages/ar.json index 0fedb1c..65c606a 100644 --- a/src/messages/ar.json +++ b/src/messages/ar.json @@ -7,6 +7,74 @@ "logo1": "رابين", "logo2": "شبكة" }, + "academy": { + "single": { + "notFound": "المقال غير موجود", + "backToAcademy": "العودة إلى الأكاديمية", + "badge": "تكنولوجيا", + "readTime": "دقيقة للقراءة", + "authorRole": "العلوم والتكنولوجيا", + "tableOfContents": "جدول المحتويات", + "readingProgress": "تقدم القراءة", + "share": "مشاركة", + "relatedArticles": "مقالات ذات صلة" + }, + "page": { + "hero": { + "badge": "أكاديمية روبين للشبكات", + "title1": "المعرفة والخبرة،", + "title2": "مشاركة", + "subtitle": "مقالات متخصصة، دروس تعليمية، وتجارب واقعية من مشاريع الشبكات والبرمجيات" + }, + "searchPlaceholder": "البحث عن المقالات...", + "featuredInCard": "شبكة", + "minutesRead": "دقيقة للقراءة", + "pagination": { + "previous": ">", + "next": "<" + }, + "tags": { + "mikrotik": "ميكروتيك", + "vlan": "VLAN", + "routing": "توجيه", + "network": "شبكة" + } + } + }, + "network": { + "hero": { + "badge": "هندسة الشبكات والأمن", + "title1": "البنية التحتية للشبكات", + "title2": "مستقرة وآمنة", + "subtitle": "تصميم وتنفيذ وإدارة البنى التحتية المعقدة للشبكات مع التركيز على الأمان والاستقرار وقابلية التوسع.", + "viewPortfolio": "عرض الأعمال", + "freeConsultation": "احصل على استشارة مجانية" + }, + "services": { + "sectionTitle": "خدمات الشبكات", + "sectionSubtitle": "حلول شاملة للبنية التحتية لشبكة مؤسستك", + "items": { + "wifi": { "title": "الشبكة اللاسلكية", "subtitle": "شبكة واي فاي للمؤسسات" }, + "security": { "title": "الأمن", "subtitle": "جدار الحماية و IDS/IPS" }, + "design": { "title": "تصميم الشبكات", "subtitle": "الهيكلة والطوبولوجيا" }, + "support": { "title": "الدعم", "subtitle": "دعم على مدار الساعة ٢٤/٧" }, + "vpn": { "title": "VPN و WAN", "subtitle": "الاتصال بين المواقع" }, + "monitoring": { "title": "المراقبة", "subtitle": "مراقبة على مدار الساعة ٢٤/٧" } + } + }, + "technologies": { + "title": "التقنيات", + "subtitle": "المعدات والتقنيات المستخدمة" + }, + "projects": { + "title": "مشاريع الشبكات", + "subtitle": "مجموعة مختارة من مشاريع الشبكات المنفذة", + "categories": { + "network": "شبكة", + "software": "برمجة" + } + } + }, "software": { "hero": { "badge": "⚙ تطوير برمجيات مخصصة", diff --git a/src/messages/en.json b/src/messages/en.json index 370c23e..3b8586b 100644 --- a/src/messages/en.json +++ b/src/messages/en.json @@ -7,6 +7,74 @@ "logo1": "Robin", "logo2": "Network" }, + "academy": { + "single": { + "notFound": "Article not found", + "backToAcademy": "Back to Academy", + "badge": "Technology", + "readTime": "min read", + "authorRole": "Science & Technology", + "tableOfContents": "Table of Contents", + "readingProgress": "Reading Progress", + "share": "Share", + "relatedArticles": "Related Articles" + }, + "page": { + "hero": { + "badge": "Robin Network Academy", + "title1": "Knowledge & Experience", + "title2": "Shared", + "subtitle": "Specialized articles, tutorials, and real-world experiences from network and software projects" + }, + "searchPlaceholder": "Search articles...", + "featuredInCard": "Network", + "minutesRead": "min read", + "pagination": { + "previous": "<", + "next": ">" + }, + "tags": { + "mikrotik": "MikroTik", + "vlan": "VLAN", + "routing": "Routing", + "network": "Network" + } + } + }, + "network": { + "hero": { + "badge": "Network & Security Engineering", + "title1": "Network Infrastructure", + "title2": "Stable & Secure", + "subtitle": "Designing, implementing, and managing complex network infrastructures with a focus on security, stability, and scalability.", + "viewPortfolio": "View Portfolio", + "freeConsultation": "Get Free Consultation" + }, + "services": { + "sectionTitle": "Network Services", + "sectionSubtitle": "Comprehensive solutions for your organization's network infrastructure", + "items": { + "wifi": { "title": "Wireless Network", "subtitle": "Enterprise WIFI" }, + "security": { "title": "Security", "subtitle": "IDS/IPS & Firewall" }, + "design": { "title": "Network Design", "subtitle": "Architecture & Topology" }, + "support": { "title": "Support", "subtitle": "24/7 Support" }, + "vpn": { "title": "VPN & WAN", "subtitle": "Site-to-Site Connectivity" }, + "monitoring": { "title": "Monitoring", "subtitle": "24/7 Surveillance" } + } + }, + "technologies": { + "title": "Technologies", + "subtitle": "Equipment and technologies used" + }, + "projects": { + "title": "Network Projects", + "subtitle": "A selection of our implemented network projects", + "categories": { + "network": "Network", + "software": "Software" + } + } + }, "software": { "hero": { "badge": "⚙ Custom Software Development", diff --git a/src/messages/fa.json b/src/messages/fa.json index b97efbf..071f732 100644 --- a/src/messages/fa.json +++ b/src/messages/fa.json @@ -7,6 +7,74 @@ "logo1": "رابین", "logo2": "شبکه" }, + "academy": { + "single": { + "notFound": "مقاله یافت نشد", + "backToAcademy": "بازگشت به آکادمی", + "badge": "تکنولوژی", + "readTime": "دقیقه مطالعه", + "authorRole": "علم و فناوری", + "tableOfContents": "فهرست مطالب", + "readingProgress": "پیشرفت مطالعه", + "share": "اشتراک‌گذاری", + "relatedArticles": "مقالات مرتبط" + }, + "page": { + "hero": { + "badge": "آکادمی شبکه رابین", + "title1": "دانش و تجربه،", + "title2": "به اشتراک گذاشته شده", + "subtitle": "مقالات تخصصی، آموزش‌ها و تجربیات واقعی از پروژه‌های شبکه و نرم‌افزار" + }, + "searchPlaceholder": "جستجوی مقالات...", + "featuredInCard": "شبکه", + "minutesRead": "دقیقه مطالعه", + "pagination": { + "previous": ">", + "next": "<" + }, + "tags": { + "mikrotik": "میکروتیک", + "vlan": "VLAN", + "routing": "مسیریابی", + "network": "شبکه" + } + } + }, + "network": { + "hero": { + "badge": "مهندسی شبکه و امنیت", + "title1": "زیرساخت شبکه", + "title2": "پایدار و امن", + "subtitle": "طراحی، پیاده‌سازی و مدیریت زیرساخت‌های شبکه پیچیده با تمرکز بر امنیت، پایداری و مقیاس‌پذیری", + "viewPortfolio": "مشاهده نمونه‌کارها", + "freeConsultation": "دریافت مشاوره رایگان" + }, + "services": { + "sectionTitle": "خدمات شبکه", + "sectionSubtitle": "راهکارهای جامع برای زیرساخت شبکه سازمان شما", + "items": { + "wifi": { "title": "شبکه بی‌سیم", "subtitle": "WIFI Enterprise" }, + "security": { "title": "امنیت", "subtitle": "IDS/IPS و Firewall" }, + "design": { "title": "طراحی شبکه", "subtitle": "معماری و توپولوژی" }, + "support": { "title": "پشتیبانی", "subtitle": "Support ۲۴/۷" }, + "vpn": { "title": "VPN & WAN", "subtitle": "اتصال بین سایت" }, + "monitoring": { "title": "مانیتورینگ", "subtitle": "نظارت ۲۴/۷" } + } + }, + "technologies": { + "title": "تکنولوژی‌ها", + "subtitle": "تجهیزات و فناوری‌های مورد استفاده" + }, + "projects": { + "title": "پروژه‌های شبکه", + "subtitle": "نمونه‌ای از پروژه‌های شبکه اجرا شده", + "categories": { + "network": "شبکه", + "software": "نرم‌افزار" + } + } + }, "software": { "hero": { "badge": "⚙ توسعه نرم‌افزار سفارشی", diff --git a/src/utilities/lib/calculate-reading-time.ts b/src/utilities/lib/calculate-reading-time.ts new file mode 100644 index 0000000..3969b8d --- /dev/null +++ b/src/utilities/lib/calculate-reading-time.ts @@ -0,0 +1,15 @@ +import { BlogTranslation } from "../types/blog.type"; + +export function calculateReadingTime(translation: BlogTranslation): number { + let text = `${translation.title} ${translation.description}`; + translation.editor.forEach((block) => { + if (block.type === "paragraph" || block.type === "heading") { + text += ` ${block.content}`; + } else if (block.type === "link") { + text += ` ${block.content.content}`; + } + }); + + const wordCount = text.trim().split(/\s+/).length; + return Math.max(1, Math.ceil(wordCount / 200)); +} diff --git a/src/utilities/lib/format-date-by-locale.ts b/src/utilities/lib/format-date-by-locale.ts index b5da35a..5aaaaa0 100644 --- a/src/utilities/lib/format-date-by-locale.ts +++ b/src/utilities/lib/format-date-by-locale.ts @@ -1,6 +1,7 @@ import { formatToPersianDate } from "./format-persian-date"; export const formatDateByLocale = (dateString: string, locale: string) => { + console.log(dateString); if (locale === "fa") { return formatToPersianDate(new Date(dateString)); } @@ -14,7 +15,6 @@ export const formatDateByLocale = (dateString: string, locale: string) => { }); } - // برای انگلیسی و سایر زبان‌ها return new Date(dateString).toLocaleDateString(locale, { year: "numeric", month: "short", diff --git a/src/utilities/types/blog.type.ts b/src/utilities/types/blog.type.ts index 82ecef1..e9d66aa 100644 --- a/src/utilities/types/blog.type.ts +++ b/src/utilities/types/blog.type.ts @@ -1,23 +1,50 @@ export interface Blog { id: string; - writer: string; featuredImage: string; gallery: string[]; href: string; - publishedAt: Date; - createdAt: Date; - updatedAt: Date; + publishedAt: string; + createdAt: string; + translations: BlogTranslation[]; + writer: { + username: string; + }; +} + +export interface BlogTranslation { + id: string; title: string; description: string; - editor: EditorDataType[]; + editor: EditorBlock[]; + language: string; + blogId: string; } -export const editorKeyOptions = ["paragraph", "heading", "link", "image"] as const; +export type EditorBlock = HeadingBlock | ParagraphBlock | ImageBlock | LinkBlock; -export type EditorKeyDataType = (typeof editorKeyOptions)[number]; - -export interface EditorDataType { - type: EditorKeyDataType; - content: any; +export interface HeadingBlock { sort: number; + type: "heading"; + content: string; +} + +export interface ParagraphBlock { + sort: number; + type: "paragraph"; + content: string; +} + +export interface ImageBlock { + sort: number; + type: "image"; + content: number; +} + +export interface LinkBlock { + sort: number; + type: "link"; + content: { + link: string; + content: string; + }; }