add readme for documentation

This commit is contained in:
Pouya Defaei
2026-05-02 08:52:08 +03:30
parent fc97bc5f59
commit 3edf6ff351
27 changed files with 927 additions and 664 deletions

View File

@@ -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.

17
ecosystem.config.js Normal file
View File

@@ -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",
},
},
],
};

View File

@@ -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 (
<div className="min-h-screen bg-[#0B1120] text-white font-sans selection:bg-orange-500/30 rtl" dir="rtl">
{/* Background Grid Pattern */}
<div className={`min-h-screen bg-[#0B1120] text-white font-sans selection:bg-orange-500/30 ${isRtl ? "rtl" : "ltr"}`} dir={isRtl ? "rtl" : "ltr"}>
<div
className="fixed inset-0 z-0 pointer-events-none opacity-20"
style={{
@@ -19,16 +41,11 @@ export default function SingleAcademyPage() {
<div className="relative z-10 pt-24 pb-12">
<div className="max-w-6xl px-4 mx-auto sm:px-6 lg:px-8">
<ArticleHeader />
<ArticleBody />
<RelatedArticles />
<ArticleHeader blog={blog} translation={translation} readingTime={readingTime} />
<ArticleBody blog={blog} translation={translation} />
<RelatedArticles data={relatedBlogs} />
</div>
</div>
<div className="relative z-10 max-w-6xl px-4 pb-12 mx-auto sm:px-6 lg:px-8">
<Consultation />
<ContactFooter />
</div>
</div>
);
}

View File

@@ -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 (
<div className="min-h-screen pt-8 font-sans bg-background text-foreground md:pt-16">
<div className="max-w-[1440px] mx-auto px-4 md:px-8">
<AcademyHeader />
<FeaturedArticle />
<ArticleGrid />
</div>
<div className="mt-16 md:mt-24">
<Consultation />
<ContactFooter />
<AcademyClient data={data || []} />
</div>
</div>
);

View File

@@ -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 (
<main className="relative flex flex-col items-center overflow-x-hidden">
<div className="fixed inset-0 -z-20 bg-grid-pattern" />
@@ -14,9 +22,7 @@ export default function NetworkPage() {
<Hero />
<Services />
<Technologies />
<Projects />
<Consultation />
<ContactFooter />
<Projects data={latestPortfolios} />
</main>
);
}

View File

@@ -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);

View File

@@ -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 (
<>
<div className="flex flex-col items-center mb-12 text-center md:mb-20">
<div className="bg-accent/10 border border-accent/20 text-accent px-4 py-1.5 rounded-full text-xs mb-6 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
{t("hero.badge")}
</div>
<h1 className="mb-4 text-3xl font-bold md:text-5xl lg:text-6xl">
{t("hero.title1")}
<span className="block mt-2 text-accent">{t("hero.title2")}</span>
</h1>
<p className="max-w-2xl mb-10 text-sm text-muted md:text-base">{t("hero.subtitle")}</p>
<div className="relative w-full max-w-2xl mb-12">
<input
type="text"
placeholder={t("searchPlaceholder")}
value={search}
onChange={({ target }) => 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"
/>
<svg
className="absolute w-5 h-5 -translate-y-1/2 text-muted ltr:right-4 rtl:left-4 top-1/2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
{featuredPost && (
<Link
href={`/academy/${featuredPost.href}`}
className="flex flex-col mb-8 overflow-hidden transition-colors border cursor-pointer bg-panel border-border rounded-2xl md:mb-12 md:flex-row group hover:border-accent/50"
>
<div className="flex flex-col flex-1 order-2 p-6 md:p-10 md:order-1">
<div className="flex items-center gap-4 mb-4 text-xs text-muted">
<span className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{formatDateByLocale(featuredPost.publishedAt, locale)}
</span>
<span className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{calculateReadingTime(featuredPost.translations[0])} {t("minutesRead")}
</span>
</div>
<h2 className="mb-4 text-2xl font-bold leading-tight transition-colors md:text-3xl lg:text-4xl group-hover:text-accent">
{featuredPost.translations?.[0]?.title}
</h2>
<p className="mb-8 text-sm leading-relaxed text-muted md:text-base line-clamp-3">{featuredPost.translations?.[0]?.description}</p>
</div>
<div className="w-full md:w-[45%] aspect-video md:h-auto bg-gradient-to-br from-[#2a1b18] to-[#121418] relative order-1 md:order-2 border-b md:border-b-0 md:ltr:border-l md:rtl:border-r border-border">
<Image
src={`${BACKEND_URL}/uploads/${featuredPost.featuredImage}`}
fill
alt={featuredPost.translations[0].title}
className="object-cover"
/>
</div>
</Link>
)}
{gridPosts.length > 0 && (
<div className="flex flex-col items-center">
<div className="grid w-full grid-cols-1 gap-6 mb-12 md:grid-cols-2 lg:grid-cols-3">
{gridPosts.map((b) => {
const translation = b.translations?.[0];
return (
<Link
href={`/academy/${b.href}`}
key={b.id}
className="flex flex-col h-full overflow-hidden transition-all border cursor-pointer bg-panel border-border rounded-xl group hover:border-accent/50 hover:-translate-y-1"
>
<div className="relative w-full border-b aspect-video bg-gradient-to-br from-gray-800 to-gray-900 border-border">
<Image src={`${BACKEND_URL}/uploads/${b.featuredImage}`} fill alt={translation.title} className="object-cover" />
</div>
<div className="flex flex-col flex-1 p-5">
<div className="flex justify-between items-center text-[11px] text-muted mb-3">
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{formatDateByLocale(b.publishedAt, locale)}
</span>
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{calculateReadingTime(translation)} {t("minutesRead")}
</span>
</div>
<h3 className="mb-2 text-lg font-bold transition-colors md:text-xl group-hover:text-accent line-clamp-2">{translation?.title}</h3>
<p className="flex-1 mb-6 text-xs leading-relaxed text-muted md:text-sm line-clamp-3">{translation?.description}</p>
</div>
</Link>
);
})}
</div>
{/* Pagination */}
{/* <div className="flex items-center gap-2 font-mono text-sm">
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
{t("pagination.previous")}
</button>
<button className="w-8 h-8 rounded-lg border border-accent bg-accent text-background font-bold flex items-center justify-center transition shadow-[0_0_10px_rgba(232,107,53,0.4)]">
1
</button>
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
{t("pagination.next")}
</button>
</div> */}
</div>
)}
</>
);
}

View File

@@ -1,58 +0,0 @@
"use client";
import { useState } from "react";
export default function AcademyHeader() {
const [activeFilter, setActiveFilter] = useState("همه مقالات");
const filters = ["همه مقالات", "شبکه", "امنیت", "برنامه‌نویسی", "DevOps"];
return (
<div className="flex flex-col items-center mb-12 text-center md:mb-20">
<div className="bg-accent/10 border border-accent/20 text-accent px-4 py-1.5 rounded-full text-xs mb-6 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
آکادمی رابین شبکه
</div>
<h1 className="mb-4 text-3xl font-bold md:text-5xl lg:text-6xl">
دانش و تجربه
<span className="block mt-2 text-accent">به اشتراک گذاشته شده</span>
</h1>
<p className="max-w-2xl mb-10 text-sm text-muted md:text-base">مقالات تخصصی، آموزشها و تجربیات واقعی از پروژههای شبکه و نرمافزار</p>
<div className="relative w-full max-w-2xl mb-12">
<input
type="text"
placeholder="جستجو در مقالات..."
className="w-full px-6 py-4 text-sm transition border bg-panel border-border rounded-xl focus:outline-none focus:border-accent"
/>
<svg className="absolute w-5 h-5 -translate-y-1/2 text-muted left-4 top-1/2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div className="flex flex-wrap justify-center gap-2 md:gap-4">
{filters.map((filter) => (
<button
key={filter}
onClick={() => setActiveFilter(filter)}
className={`px-4 py-2 rounded-lg text-sm transition-all border ${
activeFilter === filter
? "bg-accent border-accent text-background font-bold shadow-[0_0_15px_rgba(232,107,53,0.3)]"
: "bg-panel border-border text-muted hover:border-accent/50 hover:text-foreground"
}`}
>
{filter}
</button>
))}
</div>
</div>
);
}

View File

@@ -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 (
<div className="flex flex-col items-center">
<div className="grid w-full grid-cols-1 gap-6 mb-12 md:grid-cols-2 lg:grid-cols-3">
{articles.map((article, i) => (
<div
key={i}
className="flex flex-col h-full overflow-hidden transition-all border cursor-pointer bg-panel border-border rounded-xl group hover:border-accent/50 hover:-translate-y-1"
>
{/* تصویر کاور کارت */}
<div className={`h-48 w-full bg-gradient-to-br ${article.gradient} relative border-b border-border`}>
<div className="absolute top-4 right-4 bg-accent text-background text-[10px] md:text-xs font-bold px-2.5 py-1 rounded-md shadow-lg">
{article.category}
</div>
</div>
{/* محتوای کارت */}
<div className="flex flex-col flex-1 p-5">
<div className="flex justify-between items-center text-[11px] text-muted mb-3">
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
{article.date}
</span>
<span className="flex items-center gap-1">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
{article.readTime}
</span>
</div>
<h3 className="mb-2 text-lg font-bold transition-colors md:text-xl group-hover:text-accent">{article.title}</h3>
<p className="flex-1 mb-6 text-xs leading-relaxed text-muted md:text-sm">{article.desc}</p>
<div className="flex flex-wrap gap-2 mt-auto">
{article.tags.map((tag) => (
<span key={tag} className="text-[10px] bg-background border border-border px-2 py-1 rounded text-muted">
{tag}
</span>
))}
</div>
</div>
</div>
))}
</div>
{/* Pagination (صفحه‌بندی) */}
<div className="flex items-center gap-2 font-mono text-sm">
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
&gt; {/* راست چین (قبلی) */}
</button>
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
4
</button>
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
3
</button>
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
2
</button>
<button className="w-8 h-8 rounded-lg border border-accent bg-accent text-background font-bold flex items-center justify-center transition shadow-[0_0_10px_rgba(232,107,53,0.4)]">
1
</button>
<button className="flex items-center justify-center w-8 h-8 transition border rounded-lg border-border bg-panel text-muted hover:text-foreground">
&lt; {/* چپ چین (بعدی) */}
</button>
</div>
</div>
);
}

View File

@@ -1,52 +0,0 @@
export default function FeaturedArticle() {
return (
<div className="flex flex-col mb-8 overflow-hidden transition-colors border cursor-pointer bg-panel border-border rounded-2xl md:mb-12 md:flex-row group hover:border-accent/50">
<div className="flex flex-col justify-center flex-1 order-2 p-6 md:p-10 md:order-1">
<div className="flex items-center gap-4 mb-4 text-xs text-muted">
<span className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
۲ روز پیش
</span>
<span className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
۱۵ دقیقه
</span>
</div>
<h2 className="mb-4 text-2xl font-bold leading-tight transition-colors md:text-3xl lg:text-4xl group-hover:text-accent">
راهنمای کامل پیکربندی MikroTik برای شبکههای سازمانی
</h2>
<p className="mb-8 text-sm leading-relaxed text-muted md:text-base">
در این مقاله به صورت جامع نحوه پیکربندی روتر MikroTik برای یک شبکه سازمانی با ۱۰۰+ کاربر را بررسی میکنیم. از VLAN Segmentation تا QoS و
Firewall Rules.
</p>
<div className="flex flex-wrap gap-2 mt-auto">
{["MikroTik", "VLAN", "Routing"].map((tag) => (
<span
key={tag}
className="px-3 py-1 text-xs transition border rounded-md bg-background border-border text-muted hover:text-accent hover:border-accent/50"
>
{tag}
</span>
))}
</div>
</div>
{/* بخش تصویر کاور (از گرادیانت به عنوان جایگزین تصویر استفاده شده) */}
<div className="w-full md:w-[45%] h-64 md:h-auto bg-gradient-to-br from-[#2a1b18] to-[#121418] relative order-1 md:order-2 border-b md:border-b-0 md:border-r border-border">
<div className="absolute top-4 right-4 bg-accent text-background text-xs font-bold px-3 py-1.5 rounded-md">شبکه</div>
</div>
</div>
);
}

View File

@@ -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<boolean[]>(Array(24).fill(false));
@@ -22,18 +28,19 @@ export default function Hero() {
return (
<div className="flex flex-col items-center text-center mt-8 md:mt-16 px-4 md:px-5 w-full max-w-[1440px] mx-auto">
<div className="bg-accent/10 border border-accent/20 text-accent px-4 py-1.5 rounded-full text-[10px] md:text-xs mb-4 md:mb-6">
مهندسی شبکه و امنیت
{t("badge")}
</div>
<h1 className="mb-4 text-3xl font-bold leading-tight sm:text-4xl md:text-5xl lg:text-6xl">
زیرساخت شبکه <span className="block mt-1 text-accent md:mt-2">پایدار و امن</span>
{t("title1")} <span className="block mt-1 text-accent md:mt-2">{t("title2")}</span>
</h1>
<p className="max-w-2xl px-2 mb-8 text-sm text-muted md:text-base md:mb-12">
طراحی، پیادهسازی و مدیریت زیرساختهای شبکه پیچیده با تمرکز بر امنیت، پایداری و مقیاسپذیری
</p>
<p className="max-w-2xl px-2 mb-8 text-sm text-muted md:text-base md:mb-12">{t("subtitle")}</p>
{/* روتر با max-width برای محدود کردن سایز در دسکتاپ و overflow-x-auto برای موبایل */}
<div className="relative mb-12 md:mb-16 w-full max-w-4xl mx-auto shadow-[0_15px_40px_-10px_rgba(0,0,0,0.8)] md:shadow-[0_25px_60px_-10px_rgba(0,0,0,0.9)] rounded-2xl overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-[#2a2d36] scrollbar-track-transparent">
{/* Hardware Interface - Forced LTR to keep technical layout consistent in all languages */}
<div
dir="ltr"
className="relative mb-12 md:mb-16 w-full max-w-4xl mx-auto shadow-[0_15px_40px_-10px_rgba(0,0,0,0.8)] md:shadow-[0_25px_60px_-10px_rgba(0,0,0,0.9)] rounded-2xl overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-[#2a2d36] scrollbar-track-transparent"
>
<div className="min-w-[700px] w-full bg-[#181a1f] border border-[#2a2d36] rounded-2xl p-6 md:p-8 flex flex-col relative z-10">
<div className="flex items-center justify-between mb-3">
<div className="text-[#5c616f] font-mono text-xs md:text-sm tracking-wider">+CRS326-24G-2S</div>
@@ -97,11 +104,17 @@ export default function Hero() {
</div>
<div className="flex flex-col w-full gap-3 mb-12 sm:flex-row sm:w-auto md:gap-4 md:mb-16">
<button className="justify-center w-full px-6 py-3 text-sm font-bold text-center transition border rounded-lg sm:w-auto border-border text-muted hover:text-foreground hover:bg-white/5">
مشاهده نمونهکارها
<button
onClick={() => handleScrollToId("portfolios")}
className="justify-center w-full px-6 py-3 text-sm font-bold text-center transition border rounded-lg sm:w-auto border-border text-muted hover:text-foreground hover:bg-white/5"
>
{t("viewPortfolio")}
</button>
<button className="w-full sm:w-auto px-6 py-3 rounded-lg text-sm font-bold border border-border bg-panel text-foreground hover:bg-[#1a2030] hover:border-accent transition flex items-center justify-center gap-2">
دریافت مشاوره رایگان <span>&#x2039;</span>
<button
onClick={() => handleScrollToId("footer")}
className="w-full sm:w-auto px-6 py-3 rounded-lg text-sm font-bold border border-border bg-panel text-foreground hover:bg-[#1a2030] hover:border-accent transition flex items-center justify-center gap-2"
>
{t("freeConsultation")} <span>{""}</span>
</button>
</div>
</div>

View File

@@ -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<any>(null);
const sliderRef = useRef<HTMLDivElement>(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);
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;
};
const getYear = (date: Date | string) => {
return new Date(date).toLocaleDateString(locale, { year: "numeric" });
};
return (
<div className="flex justify-center w-full px-5 py-20 bg-bg">
<div className="flex justify-center w-full px-5 py-20 bg-bg" id="portfolios">
<div className="w-full max-w-6xl">
<div className="mb-8 text-right">
<h2 className="mb-2 text-3xl font-bold">پروژههای شبکه</h2>
<p className="text-sm text-muted">نمونهای از پروژههای شبکه اجرا شده</p>
<div className="flex items-end justify-between mb-8">
<div className="text-start">
<h2 className="mb-2 text-3xl font-bold">{t("title")}</h2>
<p className="text-sm text-muted">{t("subtitle")}</p>
</div>
<div className="flex flex-wrap items-center justify-between gap-4 mb-8">
<div className="flex gap-2">
{["همه", "سازمانی", "امنیتی", "بی‌سیم"].map((tab, i) => (
<button
key={i}
className={`px-4 py-2 rounded-lg text-sm border transition ${
i === 0 ? "bg-accent text-white border-accent" : "bg-transparent border-border text-muted hover:text-white"
}`}
>
{tab}
</button>
))}
</div>
<div className="flex gap-2">
<button className="px-4 py-2 text-sm bg-transparent border rounded-lg border-border text-foreground hover:bg-card">
مشاهده همه پروژهها
</button>
<button onClick={prevSlide} className="flex items-center justify-center transition border rounded-lg w-9 h-9 border-border hover:bg-card">
&lt;
{"<"}
</button>
<button onClick={nextSlide} className="flex items-center justify-center transition border rounded-lg w-9 h-9 border-border hover:bg-card">
&gt;
{">"}
</button>
</div>
</div>
{/* Slider Container with Drag Events */}
{/* Slider Container */}
<div
ref={sliderRef}
onMouseDown={handleMouseDown}
@@ -146,22 +80,25 @@ export default function Projects() {
isDragging ? "cursor-grabbing snap-none" : "cursor-grab snap-x snap-mandatory scroll-smooth"
}`}
>
{projects.map((proj, i) => (
{data.map((p) => (
<div
key={i}
// Changed mobile width to 85% to show 1.5 items
className="bg-card border border-border rounded-2xl p-6 relative overflow-hidden shadow-2xl flex flex-col group min-h-[200px] shrink-0 snap-start select-none w-[85%] sm:w-[calc(50%-10px)] lg:w-[calc(25%-15px)]"
key={p.id}
className="bg-card border border-border rounded-2xl p-6 relative overflow-hidden shadow-2xl flex flex-col group min-h-[200px] shrink-0 snap-start select-none w-[85%] sm:w-[calc(50%-10px)] lg:w-[calc(25%-15px)] text-start"
>
<div className="absolute top-0 right-0 w-full h-full bg-[radial-gradient(circle_at_top_right,rgba(199,92,67,0.15),transparent_60%)] pointer-events-none" />
<div
className={`absolute top-0 w-full h-full pointer-events-none ${isRtl ? "left-0 bg-[radial-gradient(circle_at_top_left,rgba(199,92,67,0.15),transparent_60%)]" : "right-0 bg-[radial-gradient(circle_at_top_right,rgba(199,92,67,0.15),transparent_60%)]"}`}
/>
<div className="flex items-center justify-between mb-5 text-xs">
<span className="px-3 py-1 border rounded-full text-accent bg-accent/10 border-accent/30">{proj.category}</span>
<span className="text-muted">{proj.client}</span>
<span className="px-3 py-1 border rounded-full text-accent bg-accent/10 border-accent/30">{t(`categories.${p.category}`)}</span>
<span className="text-muted">{p.employer}</span>
</div>
<h3 className="mb-3 text-lg">{proj.title}</h3>
<p className="flex-grow mb-5 text-sm text-muted">{proj.desc}</p>
<div className="flex items-center justify-between pt-4 mt-auto text-xs border-t border-border text-muted">
<span>{proj.tag}</span>
<span>{proj.year}</span>
<h3 className="mb-3 text-lg">{p.title}</h3>
<p className="flex-grow mb-5 text-sm text-muted line-clamp-3">{p.description}</p>
<div className="flex items-center justify-end pt-4 mt-auto text-xs border-t border-border text-muted">
<span>{getYear(new Date(p.createdAt))}</span>
</div>
</div>
))}

View File

@@ -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 (
<section className="relative w-full px-5 pt-20 pb-10 border-t border-border/50 bg-bg">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[80%] h-[400px] bg-[radial-gradient(ellipse_at_center,rgba(160,45,20,0.15)_0%,transparent_60%)] pointer-events-none" />
<div className="relative z-10 max-w-6xl mx-auto">
<div className="mb-12 text-right">
<h2 className="mb-2 text-3xl font-bold">خدمات شبکه</h2>
<p className="text-sm text-muted">راهکارهای جامع برای زیرساخت شبکه سازمان شما</p>
<div className="mb-12 text-start">
<h2 className="mb-2 text-3xl font-bold">{t("sectionTitle")}</h2>
<p className="text-sm text-muted">{t("sectionSubtitle")}</p>
</div>
<div className="grid grid-cols-1 gap-5 mb-20 md:grid-cols-2 lg:grid-cols-3">
{services.map((svc, i) => (
{serviceItems.map((svc, i) => (
<div key={i} className="flex flex-col items-center p-10 text-center border bg-card rounded-xl border-border">
<div className="flex items-center justify-center mb-5 text-2xl w-14 h-14 rounded-2xl bg-gradient-to-br from-accent-light to-accent">
{svc.icon}
</div>
<h3 className="text-lg mb-1.5">{svc.title}</h3>
<p className="font-sans text-xs tracking-wide text-muted">{svc.subtitle}</p>
<h3 className="text-lg mb-1.5">{t(`items.${svc.key}.title`)}</h3>
<p className="font-sans text-xs tracking-wide text-muted">{t(`items.${svc.key}.subtitle`)}</p>
</div>
))}
</div>

View File

@@ -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 (
<div className="w-full bg-[#0e131d] border-t border-white/5 py-20 px-5 flex justify-center">
<div className="w-full max-w-6xl mx-auto">
<div className="mb-12 text-right">
<h2 className="mb-2 text-3xl font-bold">تکنولوژیها</h2>
<p className="text-sm text-muted">تجهیزات و فناوریهای مورد استفاده</p>
<div className="mb-12 text-start">
<h2 className="mb-2 text-3xl font-bold">{t("title")}</h2>
<p className="text-sm text-muted">{t("subtitle")}</p>
</div>
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">

View File

@@ -8,7 +8,7 @@ export default function Footer() {
const isRtl = locale === "fa" || locale === "ar";
return (
<footer>
<footer id="footer">
<div className="w-full max-w-6xl px-5 mx-auto mt-10" id="contact_form">
<div className="relative grid grid-cols-1 gap-10 p-10 overflow-hidden border border-b-0 shadow-2xl bg-panel border-border rounded-t-3xl lg:grid-cols-2">
<div className="absolute -top-12 left-1/2 -translate-x-1/2 w-[80%] h-[100px] bg-[radial-gradient(ellipse,rgba(199,92,67,0.15),transparent_70%)] pointer-events-none" />

View File

@@ -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 (
<div className="flex flex-col gap-12 mb-20 lg:flex-row">
{/* Sidebar */}
<aside className="w-full lg:w-1/4 shrink-0">
<div className="sticky space-y-6 top-24">
{/* Table of Contents */}
{headings.length > 0 && (
<div className="p-5 border bg-slate-900/50 border-slate-800 rounded-xl">
<div className="flex items-center gap-2 mb-4 text-sm font-medium text-gray-300">
<List className="w-4 h-4 text-orange-500" />
فهرست مطالب
{t("tableOfContents")}
</div>
<ul className="space-y-3 text-sm text-gray-500">
<li className="transition-colors cursor-pointer hover:text-orange-400">مسئله اصلی: نمادها، نه مقدار</li>
<li className="transition-colors cursor-pointer hover:text-orange-400">مرحله اضافی مغز</li>
<li className="transition-colors cursor-pointer hover:text-orange-400">دیسکلکولیا چیست؟</li>
{headings.map((heading, index) => (
<li
key={index}
onClick={() => handleScrollToId(`blog_heading_${heading.sort}`)}
className="transition-colors cursor-pointer hover:text-orange-400"
>
{heading.content}
</li>
))}
</ul>
</div>
)}
{/* Reading Progress */}
<div className="p-5 border bg-slate-900/50 border-slate-800 rounded-xl">
<div className="flex items-center gap-2 mb-4 text-sm font-medium text-gray-300">
<BarChart2 className="w-4 h-4 text-orange-500" />
پیشرفت مطالعه
{t("readingProgress")}
</div>
<div className="w-full bg-slate-800 rounded-full h-1.5 mb-2">
<div className="bg-orange-500 h-1.5 rounded-full w-[35%]"></div>
</div>
</div>
{/* Tags */}
<div className="p-5 border bg-slate-900/50 border-slate-800 rounded-xl">
<div className="flex items-center gap-2 mb-4 text-sm font-medium text-gray-300">
<Tag className="w-4 h-4 text-orange-500" />
برچسبها
</div>
<div className="flex flex-wrap gap-2">
<span className="text-xs px-2.5 py-1 rounded-md border border-slate-700 text-gray-400 hover:text-white hover:border-orange-500/50 transition-colors cursor-pointer">
دیسکلکولیا
</span>
<span className="text-xs px-2.5 py-1 rounded-md border border-slate-700 text-gray-400 hover:text-white hover:border-orange-500/50 transition-colors cursor-pointer">
علوم اعصاب
</span>
<span className="text-xs px-2.5 py-1 rounded-md border border-slate-700 text-gray-400 hover:text-white hover:border-orange-500/50 transition-colors cursor-pointer">
یادگیری
</span>
<span className="text-xs px-2.5 py-1 rounded-md border border-slate-700 text-gray-400 hover:text-white hover:border-orange-500/50 transition-colors cursor-pointer">
کودکان
</span>
<div className="w-full bg-slate-800 rounded-full h-1.5 mb-2 overflow-hidden">
<div className="bg-orange-500 h-1.5 rounded-full transition-all duration-150 ease-out" style={{ width: `${readingProgress}%` }}></div>
</div>
</div>
</div>
@@ -57,94 +97,67 @@ export default function ArticleBody() {
{/* Main Content */}
<article className="w-full space-y-8 leading-8 text-gray-300 lg:w-3/4">
<p>
برخی کودکان در یادگیری ریاضی مشکل دارند و حل مسائل برایشان سخت است، در حالی که همکلاسیهایشان همان مسائل را راحت انجام میدهند. سالهاست که
پژوهشگران متوجه شدهاند بخشی از این تفاوت به چیزی به نام «اختلال یادگیری ریاضی» یا <strong>دیسکلکولیا</strong> مربوط میشود.
</p>
<p>
این وضعیت باعث میشود کودک در درک و کار با اعداد مشکل داشته باشد، حتی اگر هوش کلی او طبیعی باشد. اکنون مطالعهای تازه با استفاده از
تصویربرداری مغزی، سرنخهای دقیقتری از دلیل این اختلافها ارائه کرده است.
</p>
{/* Highlight Box */}
<div className="flex items-start gap-4 p-6 border bg-orange-500/5 border-orange-500/20 rounded-xl">
<Lightbulb className="w-6 h-6 mt-1 text-orange-500 shrink-0" />
<p className="m-0 font-medium leading-relaxed text-orange-100/90">
دانشمندان نشان دادهاند کودکانی که اختلال یادگیری ریاضی دارند، هنگام کار با نمادهای عددی رویکرد متفاوتی نسبت به دیگر کودکان دارند.
{translation.editor
.sort((a, b) => a.sort - b.sort)
.map((block, index) => {
switch (block.type) {
case "heading":
return (
<h2
id={`blog_heading_${block.sort}`}
key={index}
className="pr-3 mt-12 mb-6 text-2xl font-bold text-white border-r-4 border-orange-500"
>
{block.content}
</h2>
);
case "paragraph":
return (
<p key={index} className="whitespace-pre-line">
{block.content}
</p>
);
case "image":
const imageSrc = blog.gallery[block.content];
return imageSrc ? (
<div key={index} className="relative w-full my-8 overflow-hidden border aspect-video rounded-xl border-slate-800">
<Image fill src={`${BACKEND_URL}/uploads/${imageSrc}`} alt="Article Attachment" className="object-cover w-full h-auto" />
</div>
<p>
آنها در پاسخدادن احتیاط کمتری نشان میدهند و وقتی اشتباه میکنند، برخلاف همسالانشان سرعت خود را کم نمیکنند تا عملکردشان را اصلاح کنند.
اما نکته جالب اینجاست: وقتی همان مسئلهها به جای عدد، با «نقطه» نمایش داده شد، این تفاوتها از بین رفت.
</p>
<h2 className="pr-3 mt-12 mb-6 text-2xl font-bold text-white border-r-4 border-orange-500">مسئله اصلی: نمادها، نه مقدار</h2>
<p>
این ایده که «نمادهای عددی» برای برخی کودکان چالشبرانگیز است، موضوع تازهای نیست. <strong>برت دسمت</strong>، عصبپژوه آموزشی در دانشگاه
کییو لوون بلژیک که در پژوهش حضور نداشت، میگوید سالهاست مشاهده میشود که مشکل اصلی این کودکان بیشتر در «پردازش نمادین» است؛ یعنی درک اینکه
یک علامت نوشتاری مثل «۷» نماینده یک مقدار مشخص است.
</p>
{/* Blockquote */}
<blockquote className="p-6 my-8 border-r-2 border-orange-500/50 bg-slate-900/50 rounded-l-xl">
<p className="mb-4 text-lg font-medium leading-relaxed text-white">
مشکل اصلی این کودکان بیشتر در «پردازش نمادین» است؛ یعنی درک اینکه یک علامت نوشتاری مثل «۷» نماینده یک مقدار مشخص است.
</p>
<footer className="flex items-center gap-2 text-sm text-orange-500/80">
<div className="w-4 h-[1px] bg-orange-500/50"></div>
برت دسمت عصبپژوه آموزشی، دانشگاه کییو لوون
</footer>
</blockquote>
<h2 className="pr-3 mt-12 mb-6 text-2xl font-bold text-white border-r-4 border-orange-500">مرحله اضافی مغز</h2>
<p>
به بیان ساده، بسیاری از کودکان میتوانند وقتی با چند شیء واقعی یا چند نقطه روبهرو میشوند، مقدار را حدس بزنند یا مقایسه کنند؛ اما وقتی همان
مقدار به شکل یک نماد انتزاعی نوشته میشود، مغزشان باید یک مرحله اضافی برای ترجمه آن نماد به مفهوم عدد طی کند.
</p>
{/* Info List Box */}
<div className="p-8 my-10 border bg-slate-900/40 border-slate-800 rounded-xl">
<h3 className="flex items-center gap-2 mb-6 text-xl font-bold text-white">
<span className="w-2 h-2 bg-orange-500 rounded-full"></span>
دیسکلکولیا چیست؟
</h3>
<ul className="space-y-4">
<li className="flex items-center gap-3 text-gray-300">
<CheckCircle2 className="w-5 h-5 text-orange-500 shrink-0" />
<span>اختلال یادگیری خاص در حوزه ریاضی</span>
</li>
<li className="flex items-center gap-3 text-gray-300">
<CheckCircle2 className="w-5 h-5 text-orange-500 shrink-0" />
<span>ربطی به هوش کلی کودک ندارد</span>
</li>
<li className="flex items-center gap-3 text-gray-300">
<CheckCircle2 className="w-5 h-5 text-orange-500 shrink-0" />
<span>ریشه در پردازش نمادین اعداد دارد</span>
</li>
<li className="flex items-center gap-3 text-gray-300">
<CheckCircle2 className="w-5 h-5 text-orange-500 shrink-0" />
<span>با روشهای آموزشی مناسب قابل مدیریت است</span>
</li>
</ul>
</div>
) : null;
case "link":
return (
<a
key={index}
href={block.content.link}
target="_blank"
rel="noopener noreferrer"
className="text-orange-500 underline transition-colors hover:text-orange-400"
>
{block.content.content}
</a>
);
default:
return null;
}
})}
{/* Article Footer Actions */}
<div className="flex flex-wrap items-center justify-between gap-4 pt-8 border-t border-slate-800">
<div className="flex flex-wrap items-center justify-between gap-4 pt-8 mt-12 border-t border-slate-800">
<div className="flex items-center gap-3">
<span className="text-sm text-gray-400">اشتراکگذاری:</span>
<button className="flex items-center justify-center text-gray-400 transition-colors border rounded w-9 h-9 border-slate-700 hover:text-white hover:border-slate-500">
<span className="text-sm text-gray-400">{t("share")}:</span>
<button
onClick={handleShare}
className="flex items-center justify-center text-gray-400 transition-colors border rounded w-9 h-9 border-slate-700 hover:text-white hover:border-slate-500"
>
<Share2 className="w-4 h-4" />
</button>
<button className="flex items-center justify-center text-gray-400 transition-colors border rounded w-9 h-9 border-slate-700 hover:text-white hover:border-slate-500">
<Copy className="w-4 h-4" />
<button
onClick={handleCopy}
className={`flex items-center justify-center transition-colors border rounded w-9 h-9 border-slate-700 hover:border-slate-500 ${isCopied ? "text-green-500 border-green-500/50" : "text-gray-400 hover:text-white"}`}
>
{isCopied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
</button>
</div>
<div className="flex gap-2">
<span className="text-xs px-3 py-1.5 rounded bg-slate-900 border border-slate-800 text-gray-400">دیسکلکولیا</span>
<span className="text-xs px-3 py-1.5 rounded bg-slate-900 border border-slate-800 text-gray-400">علوم اعصاب</span>
</div>
</div>
</article>
</div>

View File

@@ -1,69 +1,101 @@
import React from "react";
import { ChevronRight, Clock, Calendar, Bookmark, Share2, User } from "lucide-react";
"use client";
import { useState } from "react";
import { ChevronRight, Clock, Calendar, Bookmark, Share2, User, Link as LinkIcon, Check } from "lucide-react";
import { Blog, BlogTranslation } from "@/utilities/types/blog.type";
import Image from "next/image";
import { BACKEND_URL } from "@/utilities/constants/urls.constant";
import { formatDateByLocale } from "@/utilities/lib/format-date-by-locale";
import { useLocale, useTranslations } from "next-intl";
interface Props {
blog: Blog;
translation: BlogTranslation;
readingTime: number;
}
export default function ArticleHeader({ blog, translation, readingTime }: Props) {
const locale = useLocale();
const t = useTranslations("academy.single");
const isRtl = locale === "fa" || locale === "ar";
const [isCopied, setIsCopied] = useState(false);
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();
}
};
export default function ArticleHeader() {
return (
<header className="mb-16">
{/* Back Button */}
<a href="/academy" className="inline-flex items-center mb-8 text-sm text-gray-400 transition-colors hover:text-orange-500">
<ChevronRight className="w-4 h-4 ml-1" />
بازگشت به آکادمی
<ChevronRight className={`w-4 h-4 ${isRtl ? "ml-1" : "mr-1 rotate-180"}`} />
{t("backToAcademy")}
</a>
{/* Meta Info */}
<div className="flex flex-wrap items-center gap-4 mb-6 text-xs text-gray-400">
<span className="px-3 py-1 text-orange-500 border rounded-full bg-orange-500/10 border-orange-500/20">علوم اعصاب</span>
<span className="px-3 py-1 text-orange-500 border rounded-full bg-orange-500/10 border-orange-500/20">{t("badge")}</span>
<div className="flex items-center">
<Clock className="w-4 h-4 ml-1.5 text-orange-500" />۸ دقیقه مطالعه
<Clock className={`w-4 h-4 text-orange-500 ${isRtl ? "ml-1.5" : "mr-1.5"}`} />
{readingTime} {t("readTime")}
</div>
<div className="flex items-center">
<Calendar className="w-4 h-4 ml-1.5 text-orange-500" />
۱۰ فروردین ۱۴۰۴
<Calendar className={`w-4 h-4 text-orange-500 ${isRtl ? "ml-1.5" : "mr-1.5"}`} />
{formatDateByLocale(blog.publishedAt.toString(), locale)}
</div>
</div>
{/* Title & Excerpt */}
<h1 className="mb-6 text-4xl font-bold leading-tight md:text-5xl">دیسکلکولیا: وقتی مغز با اعداد کنار نمیآید</h1>
<div className="pr-4 mb-8 border-r-2 border-orange-500">
<p className="text-lg leading-relaxed text-gray-300">
پژوهش جدید با تصویربرداری مغزی نشان میدهد مشکل اصلی کودکان مبتلا به اختلال یادگیری ریاضی، نه در درک مقدار، بلکه در پردازش نمادهای عددی
نهفته است.
</p>
<h1 className="mb-6 text-4xl font-bold leading-tight md:text-5xl">{translation.title}</h1>
<div className={`mb-8 border-orange-500 ${isRtl ? "pr-4 border-r-2" : "pl-4 border-l-2"}`}>
<p className="text-lg leading-relaxed text-gray-300">{translation.description}</p>
</div>
{/* Author & Actions */}
<div className="flex items-center justify-between mb-12">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-10 h-10 text-orange-500 border rounded-full bg-slate-800 border-slate-700">
<User className="w-5 h-5" />
</div>
<div>
<div className="text-sm font-medium">تیم آکادمی رابین</div>
<div className="text-xs text-gray-400">علم و فناوری</div>
<div className="text-sm font-medium">{blog.writer.username}</div>
<div className="text-xs text-gray-400">{t("authorRole")}</div>
</div>
</div>
<div className="flex items-center gap-2">
<button className="flex items-center justify-center w-10 h-10 text-gray-400 transition-all border rounded-lg border-slate-800 bg-slate-900/50 hover:text-white hover:border-slate-600">
<button
onClick={handleShare}
className="flex items-center justify-center w-10 h-10 text-gray-400 transition-all border rounded-lg border-slate-800 bg-slate-900/50 hover:text-white hover:border-slate-600"
>
<Share2 className="w-4 h-4" />
</button>
<button className="flex items-center justify-center w-10 h-10 text-gray-400 transition-all border rounded-lg border-slate-800 bg-slate-900/50 hover:text-white hover:border-slate-600">
<Bookmark className="w-4 h-4" />
<button
onClick={handleCopy}
className={`flex items-center justify-center w-10 h-10 transition-all border rounded-lg border-slate-800 bg-slate-900/50 hover:border-slate-600 ${isCopied ? "text-green-500 border-green-500/50" : "text-gray-400 hover:text-white"}`}
>
{isCopied ? <Check className="w-4 h-4" /> : <LinkIcon className="w-4 h-4" />}
</button>
</div>
</div>
{/* Hero Image */}
<div className="w-full aspect-[21/9] rounded-2xl border border-slate-800 bg-gradient-to-br from-slate-900 to-slate-800 relative overflow-hidden flex items-center justify-center">
{/* Placeholder for actual image - representing the brain and numbers network */}
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-orange-500/10 via-transparent to-transparent"></div>
<div className="flex flex-col items-center text-orange-500/50">
<span className="mb-4 text-6xl">🧠</span>
<div className="absolute p-2 text-sm border rounded-lg top-1/4 left-1/3 border-slate-700 bg-slate-800/50">۳</div>
<div className="absolute p-2 text-sm border rounded-lg bottom-1/3 right-1/4 border-slate-700 bg-slate-800/50">۷</div>
<div className="absolute p-2 text-sm border rounded-lg top-1/2 left-1/4 border-slate-700 bg-slate-800/50">۵</div>
<div className="absolute p-2 text-sm border rounded-lg top-1/3 right-1/3 border-slate-700 bg-slate-800/50">۹</div>
</div>
{blog.featuredImage && (
<div className="relative flex items-center justify-center w-full overflow-hidden border aspect-video rounded-2xl border-slate-800 bg-gradient-to-br from-slate-900 to-slate-800">
<Image src={`${BACKEND_URL}/uploads/${blog.featuredImage}`} alt={translation.title} fill className="object-cover w-full h-full" />
</div>
)}
</header>
);
}

View File

@@ -1,57 +1,53 @@
import React from "react";
import { BookOpen, Clock } from "lucide-react";
import { Blog } from "@/utilities/types/blog.type";
import { calculateReadingTime } from "@/utilities/lib/calculate-reading-time";
import Image from "next/image";
import { BACKEND_URL } from "@/utilities/constants/urls.constant";
import { useTranslations, useLocale } from "next-intl";
import Link from "next/link";
import { formatDateByLocale } from "@/utilities/lib/format-date-by-locale";
export default function RelatedArticles() {
const articles = [
{
id: 1,
category: "امنیت",
title: "۱۰ نکته طلایی برای امن‌سازی Firewall",
time: "۱۲ دقیقه",
gradient: "from-slate-800 to-slate-900",
},
{
id: 2,
category: "شبکه",
title: "راهنمای کامل پیکربندی MikroTik",
time: "۱۵ دقیقه",
gradient: "from-slate-800 to-slate-900",
},
{
id: 3,
category: "DEVOPS",
title: "راه‌اندازی CI/CD Pipeline با GitHub Actions",
time: "۲۵ دقیقه",
gradient: "from-orange-900/20 to-slate-900",
},
];
export default function RelatedArticles({ data }: { data: Blog[] }) {
const t = useTranslations("academy.single");
const locale = useLocale();
const isRtl = locale === "fa" || locale === "ar";
if (!data || data.length === 0) return null;
return (
<section className="pt-16 mb-16 border-t border-slate-800">
<div className="flex items-center justify-between mb-8">
<h2 className="flex items-center gap-2 text-xl font-bold text-white">
<BookOpen className="w-5 h-5 text-orange-500" />
مقالات مرتبط
{t("relatedArticles")}
</h2>
</div>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{articles.map((article) => (
<div
key={article.id}
{data.map((b) => {
const translation = b.translations.find((t) => t.language === locale) || b.translations[0];
return (
<Link
href={`/${locale}/academy/${b.href}`}
key={b.id}
className="overflow-hidden transition-all duration-300 border cursor-pointer group rounded-2xl border-slate-800 bg-slate-900/50 hover:border-orange-500/50"
>
<div className={`w-full h-32 bg-gradient-to-br ${article.gradient} opacity-50 group-hover:opacity-100 transition-opacity`}></div>
<div className="relative w-full transition-opacity aspect-video bg-gradient-to-br">
<Image src={`${BACKEND_URL}/uploads/${b.featuredImage}`} fill alt={translation.title} className="object-cover" />
</div>
<div className="p-5">
<span className="block mb-3 text-xs font-medium text-orange-500">{article.category}</span>
<h3 className="mb-4 font-medium text-white transition-colors group-hover:text-orange-400 line-clamp-2">{article.title}</h3>
<div className="flex items-center text-xs text-gray-500">
<Clock className="w-3.5 h-3.5 ml-1" />
{article.time}
<span className="flex gap-1 mb-3 text-xs font-medium text-orange-500">
<Clock className={`w-3.5 h-3.5 ${isRtl ? "ml-1" : "mr-1"}`} />
{calculateReadingTime(translation)} {t("readTime")}
</span>
<h3 className="mb-4 font-medium text-white transition-colors group-hover:text-orange-400 line-clamp-2">{translation.title}</h3>
<div className="flex items-center gap-1 text-xs text-gray-500">{formatDateByLocale(b.publishedAt.toString(), locale)}</div>
</div>
</div>
</div>
))}
</Link>
);
})}
</div>
</section>
);

View File

@@ -56,7 +56,7 @@ export default function Projects({ data }: { data: Portfolio[] }) {
};
return (
<section className="px-4 py-12 sm:px-6 sm:py-16">
<section className="px-4 py-12 sm:px-6 sm:py-16" id="portfolios">
<div className="flex flex-wrap items-end justify-between gap-4 mx-auto mb-8 sm:mb-12 max-w-7xl">
<div className="text-start">
<h2 className="mb-2 text-2xl font-bold sm:text-3xl">{t("sectionTitle")}</h2>
@@ -70,14 +70,14 @@ export default function Projects({ data }: { data: Portfolio[] }) {
className="flex items-center justify-center transition border rounded-lg w-9 h-9 sm:w-10 sm:h-10 border-border hover:bg-card bg-card text-foreground"
aria-label="Scroll previous"
>
{isRTL ? "<" : "<"}
{"<"}
</button>
<button
onClick={() => scroll("next")}
className="flex items-center justify-center transition border rounded-lg w-9 h-9 sm:w-10 sm:h-10 border-border hover:bg-card bg-card text-foreground"
aria-label="Scroll next"
>
{isRTL ? ">" : ">"}
{">"}
</button>
</div>
</div>
@@ -93,9 +93,9 @@ export default function Projects({ data }: { data: Portfolio[] }) {
isDragging ? "cursor-grabbing snap-none" : "cursor-grab snap-x snap-mandatory scroll-smooth"
}`}
>
{[...data, ...data, ...data, ...data, ...data, ...data].map((p, i) => (
{data.map((p) => (
<div
key={i}
key={p.id}
className="flex-shrink-0 w-[85%] sm:w-[320px] lg:w-[380px] p-5 sm:p-6 text-start border bg-card border-border rounded-xl snap-start select-none flex flex-col min-h-[200px]"
>
<div className="flex items-start justify-between mb-3 sm:mb-4">
@@ -109,7 +109,7 @@ export default function Projects({ data }: { data: Portfolio[] }) {
<div className="flex items-center justify-between pt-3 mt-auto border-t sm:pt-4 border-border">
<button className="text-xs transition-colors sm:text-sm text-accent hover:text-accent/80">
{t("viewDetails")} {isRTL ? ">" : ">"}
{t("viewDetails")} {">"}
</button>
</div>
</div>

View File

@@ -1,12 +1,8 @@
import { useLocale, useTranslations } from "next-intl";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useTranslations } from "next-intl";
import HeroButtons from "./HeroButtons";
export default function Hero() {
const t = useTranslations("software.hero");
const locale = useLocale();
const isRtl = locale === "fa" || locale === "ar";
const ArrowIcon = isRtl ? ChevronLeft : ChevronRight;
return (
<section className="px-4 py-12 text-center sm:py-16 md:py-20">
@@ -42,14 +38,7 @@ export default function Hero() {
</div>
</div>
<div className="flex flex-col justify-center gap-3 px-4 sm:flex-row sm:gap-4">
<button className="flex items-center justify-center gap-2 px-5 py-2.5 sm:px-6 sm:py-3 text-sm sm:text-base text-white transition rounded-lg bg-accent hover:bg-accent/90">
{t("buttons.startProject")} <ArrowIcon className="w-4 h-4" />
</button>
<button className="px-5 py-2.5 sm:px-6 sm:py-3 text-sm sm:text-base transition border rounded-lg border-border hover:bg-card">
{t("buttons.viewPortfolio")}
</button>
</div>
<HeroButtons />
</section>
);
}

View File

@@ -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 (
<div className="flex flex-col justify-center gap-3 px-4 sm:flex-row sm:gap-4">
<button
onClick={() => handleScrollToId("footer")}
className="flex items-center justify-center gap-2 px-5 py-2.5 sm:px-6 sm:py-3 text-sm sm:text-base text-white transition rounded-lg bg-accent hover:bg-accent/90"
>
{t("buttons.startProject")} <ArrowIcon className="w-4 h-4" />
</button>
<button
onClick={() => handleScrollToId("portfolios")}
className="px-5 py-2.5 sm:px-6 sm:py-3 text-sm sm:text-base transition border rounded-lg border-border hover:bg-card"
>
{t("buttons.viewPortfolio")}
</button>
</div>
);
}

View File

@@ -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": "⚙ تطوير برمجيات مخصصة",

View File

@@ -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",

View File

@@ -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": "⚙ توسعه نرم‌افزار سفارشی",

View File

@@ -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));
}

View File

@@ -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",

View File

@@ -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;
};
}