add readme for documentation
This commit is contained in:
57
README.md
57
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
|
```bash
|
||||||
npm run dev
|
npm install
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
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
17
ecosystem.config.js
Normal 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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
@@ -1,13 +1,35 @@
|
|||||||
import Consultation from "@/components/network/Consultation";
|
|
||||||
import ContactFooter from "@/components/network/ContactFooter";
|
|
||||||
import ArticleBody from "@/components/single-academy/ArticleBody";
|
import ArticleBody from "@/components/single-academy/ArticleBody";
|
||||||
import ArticleHeader from "@/components/single-academy/ArticleHeader";
|
import ArticleHeader from "@/components/single-academy/ArticleHeader";
|
||||||
import RelatedArticles from "@/components/single-academy/RelatedArticles";
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-[#0B1120] text-white font-sans selection:bg-orange-500/30 rtl" dir="rtl">
|
<div className={`min-h-screen bg-[#0B1120] text-white font-sans selection:bg-orange-500/30 ${isRtl ? "rtl" : "ltr"}`} dir={isRtl ? "rtl" : "ltr"}>
|
||||||
{/* Background Grid Pattern */}
|
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-0 pointer-events-none opacity-20"
|
className="fixed inset-0 z-0 pointer-events-none opacity-20"
|
||||||
style={{
|
style={{
|
||||||
@@ -19,16 +41,11 @@ export default function SingleAcademyPage() {
|
|||||||
|
|
||||||
<div className="relative z-10 pt-24 pb-12">
|
<div className="relative z-10 pt-24 pb-12">
|
||||||
<div className="max-w-6xl px-4 mx-auto sm:px-6 lg:px-8">
|
<div className="max-w-6xl px-4 mx-auto sm:px-6 lg:px-8">
|
||||||
<ArticleHeader />
|
<ArticleHeader blog={blog} translation={translation} readingTime={readingTime} />
|
||||||
<ArticleBody />
|
<ArticleBody blog={blog} translation={translation} />
|
||||||
<RelatedArticles />
|
<RelatedArticles data={relatedBlogs} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import AcademyHeader from "@/components/academy/AcademyHeader";
|
import AcademyClient from "@/components/academy/AcademyClient";
|
||||||
import ArticleGrid from "@/components/academy/ArticleGrid";
|
import { BACKEND_URL_LOCAL } from "@/utilities/constants/urls.constant";
|
||||||
import FeaturedArticle from "@/components/academy/FeaturedArticle";
|
import { Blog } from "@/utilities/types/blog.type";
|
||||||
import Consultation from "@/components/network/Consultation";
|
|
||||||
import ContactFooter from "@/components/network/ContactFooter";
|
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 (
|
return (
|
||||||
<div className="min-h-screen pt-8 font-sans bg-background text-foreground md:pt-16">
|
<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">
|
<div className="max-w-[1440px] mx-auto px-4 md:px-8">
|
||||||
<AcademyHeader />
|
<AcademyClient data={data || []} />
|
||||||
<FeaturedArticle />
|
|
||||||
<ArticleGrid />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-16 md:mt-24">
|
|
||||||
<Consultation />
|
|
||||||
<ContactFooter />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,8 +4,16 @@ import Technologies from "@/components/network/Technologies";
|
|||||||
import Projects from "@/components/network/Projects";
|
import Projects from "@/components/network/Projects";
|
||||||
import Consultation from "@/components/network/Consultation";
|
import Consultation from "@/components/network/Consultation";
|
||||||
import ContactFooter from "@/components/network/ContactFooter";
|
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 (
|
return (
|
||||||
<main className="relative flex flex-col items-center overflow-x-hidden">
|
<main className="relative flex flex-col items-center overflow-x-hidden">
|
||||||
<div className="fixed inset-0 -z-20 bg-grid-pattern" />
|
<div className="fixed inset-0 -z-20 bg-grid-pattern" />
|
||||||
@@ -14,9 +22,7 @@ export default function NetworkPage() {
|
|||||||
<Hero />
|
<Hero />
|
||||||
<Services />
|
<Services />
|
||||||
<Technologies />
|
<Technologies />
|
||||||
<Projects />
|
<Projects data={latestPortfolios} />
|
||||||
<Consultation />
|
|
||||||
<ContactFooter />
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import Hero from "@/components/software/Hero";
|
import Hero from "@/components/software/hero/Hero";
|
||||||
import Services from "@/components/software/Services";
|
import Services from "@/components/software/Services";
|
||||||
import TechStack from "@/components/software/TechStack";
|
import TechStack from "@/components/software/TechStack";
|
||||||
import Process from "@/components/software/Process";
|
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 }> }) {
|
export default async function SoftwarePage({ params }: { params: Promise<{ locale: string }> }) {
|
||||||
const { locale } = await params;
|
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.json())
|
||||||
.then((res) => res.data);
|
.then((res) => res.data);
|
||||||
|
|
||||||
|
|||||||
178
src/components/academy/AcademyClient.tsx
Normal file
178
src/components/academy/AcademyClient.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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">
|
|
||||||
> {/* راست چین (قبلی) */}
|
|
||||||
</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">
|
|
||||||
< {/* چپ چین (بعدی) */}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import { useTranslations, useLocale } from "next-intl";
|
||||||
|
import { handleScrollToId } from "@/utilities/lib/scroll";
|
||||||
|
|
||||||
export default function Hero() {
|
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 [activeIndicators, setActiveIndicators] = useState({ sys: true, act: false, pwr: true });
|
||||||
const [activeSfp, setActiveSfp] = useState([true, false]);
|
const [activeSfp, setActiveSfp] = useState([true, false]);
|
||||||
const [activeRj45, setActiveRj45] = useState<boolean[]>(Array(24).fill(false));
|
const [activeRj45, setActiveRj45] = useState<boolean[]>(Array(24).fill(false));
|
||||||
@@ -22,18 +28,19 @@ export default function Hero() {
|
|||||||
return (
|
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="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">
|
<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>
|
</div>
|
||||||
|
|
||||||
<h1 className="mb-4 text-3xl font-bold leading-tight sm:text-4xl md:text-5xl lg:text-6xl">
|
<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>
|
</h1>
|
||||||
<p className="max-w-2xl px-2 mb-8 text-sm text-muted md:text-base md:mb-12">
|
<p className="max-w-2xl px-2 mb-8 text-sm text-muted md:text-base md:mb-12">{t("subtitle")}</p>
|
||||||
طراحی، پیادهسازی و مدیریت زیرساختهای شبکه پیچیده با تمرکز بر امنیت، پایداری و مقیاسپذیری
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* روتر با max-width برای محدود کردن سایز در دسکتاپ و overflow-x-auto برای موبایل */}
|
{/* Hardware Interface - Forced LTR to keep technical layout consistent in all languages */}
|
||||||
<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">
|
<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="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="flex items-center justify-between mb-3">
|
||||||
<div className="text-[#5c616f] font-mono text-xs md:text-sm tracking-wider">+CRS326-24G-2S</div>
|
<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>
|
||||||
|
|
||||||
<div className="flex flex-col w-full gap-3 mb-12 sm:flex-row sm:w-auto md:gap-4 md:mb-16">
|
<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>
|
||||||
<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">
|
<button
|
||||||
دریافت مشاوره رایگان <span>‹</span>
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,141 +1,75 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
|
import { useTranslations, useLocale } from "next-intl";
|
||||||
|
import { Portfolio } from "@/utilities/types/portfolio.type";
|
||||||
|
|
||||||
export default function Projects() {
|
export default function Projects({ data }: { data: Portfolio[] }) {
|
||||||
const projects = [
|
const t = useTranslations("network.projects");
|
||||||
{
|
const locale = useLocale();
|
||||||
category: "شبکه بیسیم",
|
const isRtl = locale === "fa" || locale === "ar";
|
||||||
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: "۲۰۲۳",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const sliderRef = useRef<any>(null);
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [startX, setStartX] = useState(0);
|
const [startX, setStartX] = useState(0);
|
||||||
const [scrollLeft, setScrollLeft] = useState(0);
|
const [scrollLeft, setScrollLeft] = useState(0);
|
||||||
|
|
||||||
|
const scrollAmount = 320;
|
||||||
|
|
||||||
const nextSlide = () => {
|
const nextSlide = () => {
|
||||||
if (sliderRef.current) {
|
if (sliderRef.current) {
|
||||||
sliderRef.current.scrollBy({ left: -320, behavior: "smooth" });
|
sliderRef.current.scrollBy({ left: isRtl ? -scrollAmount : scrollAmount, behavior: "smooth" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevSlide = () => {
|
const prevSlide = () => {
|
||||||
if (sliderRef.current) {
|
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: React.MouseEvent) => {
|
||||||
const handleMouseDown = (e: any) => {
|
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
|
if (sliderRef.current) {
|
||||||
setStartX(e.pageX - sliderRef.current.offsetLeft);
|
setStartX(e.pageX - sliderRef.current.offsetLeft);
|
||||||
setScrollLeft(sliderRef.current.scrollLeft);
|
setScrollLeft(sliderRef.current.scrollLeft);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseLeave = () => {
|
const handleMouseLeave = () => setIsDragging(false);
|
||||||
setIsDragging(false);
|
const handleMouseUp = () => setIsDragging(false);
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const handleMouseMove = (e: React.MouseEvent) => {
|
||||||
setIsDragging(false);
|
if (!isDragging || !sliderRef.current) return;
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (e: any) => {
|
|
||||||
if (!isDragging) return;
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const x = e.pageX - sliderRef.current.offsetLeft;
|
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;
|
sliderRef.current.scrollLeft = scrollLeft - walk;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getYear = (date: Date | string) => {
|
||||||
|
return new Date(date).toLocaleDateString(locale, { year: "numeric" });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
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="w-full max-w-6xl">
|
||||||
<div className="mb-8 text-right">
|
<div className="flex items-end justify-between mb-8">
|
||||||
<h2 className="mb-2 text-3xl font-bold">پروژههای شبکه</h2>
|
<div className="text-start">
|
||||||
<p className="text-sm text-muted">نمونهای از پروژههای شبکه اجرا شده</p>
|
<h2 className="mb-2 text-3xl font-bold">{t("title")}</h2>
|
||||||
|
<p className="text-sm text-muted">{t("subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4 mb-8">
|
|
||||||
<div className="flex gap-2">
|
<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">
|
<button onClick={prevSlide} className="flex items-center justify-center transition border rounded-lg w-9 h-9 border-border hover:bg-card">
|
||||||
<
|
{"<"}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={nextSlide} className="flex items-center justify-center transition border rounded-lg w-9 h-9 border-border hover:bg-card">
|
<button onClick={nextSlide} className="flex items-center justify-center transition border rounded-lg w-9 h-9 border-border hover:bg-card">
|
||||||
>
|
{">"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Slider Container with Drag Events */}
|
{/* Slider Container */}
|
||||||
<div
|
<div
|
||||||
ref={sliderRef}
|
ref={sliderRef}
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
@@ -146,22 +80,25 @@ export default function Projects() {
|
|||||||
isDragging ? "cursor-grabbing snap-none" : "cursor-grab snap-x snap-mandatory scroll-smooth"
|
isDragging ? "cursor-grabbing snap-none" : "cursor-grab snap-x snap-mandatory scroll-smooth"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{projects.map((proj, i) => (
|
{data.map((p) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={p.id}
|
||||||
// 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)] text-start"
|
||||||
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)]"
|
|
||||||
>
|
>
|
||||||
<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">
|
<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="px-3 py-1 border rounded-full text-accent bg-accent/10 border-accent/30">{t(`categories.${p.category}`)}</span>
|
||||||
<span className="text-muted">{proj.client}</span>
|
<span className="text-muted">{p.employer}</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mb-3 text-lg">{proj.title}</h3>
|
|
||||||
<p className="flex-grow mb-5 text-sm text-muted">{proj.desc}</p>
|
<h3 className="mb-3 text-lg">{p.title}</h3>
|
||||||
<div className="flex items-center justify-between pt-4 mt-auto text-xs border-t border-border text-muted">
|
<p className="flex-grow mb-5 text-sm text-muted line-clamp-3">{p.description}</p>
|
||||||
<span>{proj.tag}</span>
|
|
||||||
<span>{proj.year}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,29 +1,35 @@
|
|||||||
export default function ServicesSection() {
|
"use client";
|
||||||
const services = [
|
|
||||||
{ title: "شبکه بیسیم", subtitle: "WIFI Enterprise", icon: "📶" },
|
import { useTranslations } from "next-intl";
|
||||||
{ title: "امنیت", subtitle: "IDS/IPS و Firewall", icon: "🛡️" },
|
|
||||||
{ title: "طراحی شبکه", subtitle: "معماری و توپولوژی", icon: "📐" },
|
export default function Services() {
|
||||||
{ title: "پشتیبانی", subtitle: "Support ۲۴/۷", icon: "🎧" },
|
const t = useTranslations("network.services");
|
||||||
{ title: "VPN & WAN", subtitle: "اتصال بین سایت", icon: "🔗" },
|
|
||||||
{ title: "مانیتورینگ", subtitle: "نظارت ۲۴/۷", icon: "📈" },
|
const serviceItems = [
|
||||||
|
{ key: "wifi", icon: "📶" },
|
||||||
|
{ key: "security", icon: "🛡️" },
|
||||||
|
{ key: "design", icon: "📐" },
|
||||||
|
{ key: "support", icon: "🎧" },
|
||||||
|
{ key: "vpn", icon: "🔗" },
|
||||||
|
{ key: "monitoring", icon: "📈" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full px-5 pt-20 pb-10 border-t border-border/50 bg-bg">
|
<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="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="relative z-10 max-w-6xl mx-auto">
|
||||||
<div className="mb-12 text-right">
|
<div className="mb-12 text-start">
|
||||||
<h2 className="mb-2 text-3xl font-bold">خدمات شبکه</h2>
|
<h2 className="mb-2 text-3xl font-bold">{t("sectionTitle")}</h2>
|
||||||
<p className="text-sm text-muted">راهکارهای جامع برای زیرساخت شبکه سازمان شما</p>
|
<p className="text-sm text-muted">{t("sectionSubtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-5 mb-20 md:grid-cols-2 lg:grid-cols-3">
|
<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 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">
|
<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}
|
{svc.icon}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-lg mb-1.5">{svc.title}</h3>
|
<h3 className="text-lg mb-1.5">{t(`items.${svc.key}.title`)}</h3>
|
||||||
<p className="font-sans text-xs tracking-wide text-muted">{svc.subtitle}</p>
|
<p className="font-sans text-xs tracking-wide text-muted">{t(`items.${svc.key}.subtitle`)}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function Technologies() {
|
export default function Technologies() {
|
||||||
|
const t = useTranslations("network.technologies");
|
||||||
|
|
||||||
const techs = [
|
const techs = [
|
||||||
{
|
{
|
||||||
name: "VPN",
|
name: "VPN",
|
||||||
@@ -53,9 +59,9 @@ export default function Technologies() {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full bg-[#0e131d] border-t border-white/5 py-20 px-5 flex justify-center">
|
<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="w-full max-w-6xl mx-auto">
|
||||||
<div className="mb-12 text-right">
|
<div className="mb-12 text-start">
|
||||||
<h2 className="mb-2 text-3xl font-bold">تکنولوژیها</h2>
|
<h2 className="mb-2 text-3xl font-bold">{t("title")}</h2>
|
||||||
<p className="text-sm text-muted">تجهیزات و فناوریهای مورد استفاده</p>
|
<p className="text-sm text-muted">{t("subtitle")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-6">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function Footer() {
|
|||||||
const isRtl = locale === "fa" || locale === "ar";
|
const isRtl = locale === "fa" || locale === "ar";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer>
|
<footer id="footer">
|
||||||
<div className="w-full max-w-6xl px-5 mx-auto mt-10" id="contact_form">
|
<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="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" />
|
<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" />
|
||||||
|
|||||||
@@ -1,55 +1,95 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { List, BarChart2, Tag, Lightbulb, CheckCircle2, Share2, Copy } from "lucide-react";
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="flex flex-col gap-12 mb-20 lg:flex-row">
|
<div className="flex flex-col gap-12 mb-20 lg:flex-row">
|
||||||
{/* Sidebar */}
|
|
||||||
<aside className="w-full lg:w-1/4 shrink-0">
|
<aside className="w-full lg:w-1/4 shrink-0">
|
||||||
<div className="sticky space-y-6 top-24">
|
<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="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">
|
<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" />
|
<List className="w-4 h-4 text-orange-500" />
|
||||||
فهرست مطالب
|
{t("tableOfContents")}
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-3 text-sm text-gray-500">
|
<ul className="space-y-3 text-sm text-gray-500">
|
||||||
<li className="transition-colors cursor-pointer hover:text-orange-400">مسئله اصلی: نمادها، نه مقدار</li>
|
{headings.map((heading, index) => (
|
||||||
<li className="transition-colors cursor-pointer hover:text-orange-400">مرحله اضافی مغز</li>
|
<li
|
||||||
<li className="transition-colors cursor-pointer hover:text-orange-400">دیسکلکولیا چیست؟</li>
|
key={index}
|
||||||
|
onClick={() => handleScrollToId(`blog_heading_${heading.sort}`)}
|
||||||
|
className="transition-colors cursor-pointer hover:text-orange-400"
|
||||||
|
>
|
||||||
|
{heading.content}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Reading Progress */}
|
{/* Reading Progress */}
|
||||||
<div className="p-5 border bg-slate-900/50 border-slate-800 rounded-xl">
|
<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">
|
<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" />
|
<BarChart2 className="w-4 h-4 text-orange-500" />
|
||||||
پیشرفت مطالعه
|
{t("readingProgress")}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-slate-800 rounded-full h-1.5 mb-2">
|
<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 w-[35%]"></div>
|
<div className="bg-orange-500 h-1.5 rounded-full transition-all duration-150 ease-out" style={{ width: `${readingProgress}%` }}></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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,94 +97,67 @@ export default function ArticleBody() {
|
|||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<article className="w-full space-y-8 leading-8 text-gray-300 lg:w-3/4">
|
<article className="w-full space-y-8 leading-8 text-gray-300 lg:w-3/4">
|
||||||
<p>
|
{translation.editor
|
||||||
برخی کودکان در یادگیری ریاضی مشکل دارند و حل مسائل برایشان سخت است، در حالی که همکلاسیهایشان همان مسائل را راحت انجام میدهند. سالهاست که
|
.sort((a, b) => a.sort - b.sort)
|
||||||
پژوهشگران متوجه شدهاند بخشی از این تفاوت به چیزی به نام «اختلال یادگیری ریاضی» یا <strong>دیسکلکولیا</strong> مربوط میشود.
|
.map((block, index) => {
|
||||||
</p>
|
switch (block.type) {
|
||||||
<p>
|
case "heading":
|
||||||
این وضعیت باعث میشود کودک در درک و کار با اعداد مشکل داشته باشد، حتی اگر هوش کلی او طبیعی باشد. اکنون مطالعهای تازه با استفاده از
|
return (
|
||||||
تصویربرداری مغزی، سرنخهای دقیقتری از دلیل این اختلافها ارائه کرده است.
|
<h2
|
||||||
</p>
|
id={`blog_heading_${block.sort}`}
|
||||||
|
key={index}
|
||||||
{/* Highlight Box */}
|
className="pr-3 mt-12 mb-6 text-2xl font-bold text-white border-r-4 border-orange-500"
|
||||||
<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" />
|
{block.content}
|
||||||
<p className="m-0 font-medium leading-relaxed text-orange-100/90">
|
</h2>
|
||||||
دانشمندان نشان دادهاند کودکانی که اختلال یادگیری ریاضی دارند، هنگام کار با نمادهای عددی رویکرد متفاوتی نسبت به دیگر کودکان دارند.
|
);
|
||||||
|
case "paragraph":
|
||||||
|
return (
|
||||||
|
<p key={index} className="whitespace-pre-line">
|
||||||
|
{block.content}
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
) : null;
|
||||||
<p>
|
case "link":
|
||||||
آنها در پاسخدادن احتیاط کمتری نشان میدهند و وقتی اشتباه میکنند، برخلاف همسالانشان سرعت خود را کم نمیکنند تا عملکردشان را اصلاح کنند.
|
return (
|
||||||
اما نکته جالب اینجاست: وقتی همان مسئلهها به جای عدد، با «نقطه» نمایش داده شد، این تفاوتها از بین رفت.
|
<a
|
||||||
</p>
|
key={index}
|
||||||
|
href={block.content.link}
|
||||||
<h2 className="pr-3 mt-12 mb-6 text-2xl font-bold text-white border-r-4 border-orange-500">مسئله اصلی: نمادها، نه مقدار</h2>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
<p>
|
className="text-orange-500 underline transition-colors hover:text-orange-400"
|
||||||
این ایده که «نمادهای عددی» برای برخی کودکان چالشبرانگیز است، موضوع تازهای نیست. <strong>برت دسمت</strong>، عصبپژوه آموزشی در دانشگاه
|
>
|
||||||
کییو لوون بلژیک که در پژوهش حضور نداشت، میگوید سالهاست مشاهده میشود که مشکل اصلی این کودکان بیشتر در «پردازش نمادین» است؛ یعنی درک اینکه
|
{block.content.content}
|
||||||
یک علامت نوشتاری مثل «۷» نماینده یک مقدار مشخص است.
|
</a>
|
||||||
</p>
|
);
|
||||||
|
default:
|
||||||
{/* Blockquote */}
|
return null;
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Article Footer Actions */}
|
{/* 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">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-sm text-gray-400">اشتراکگذاری:</span>
|
<span className="text-sm text-gray-400">{t("share")}:</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">
|
<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" />
|
<Share2 className="w-4 h-4" />
|
||||||
</button>
|
</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">
|
<button
|
||||||
<Copy className="w-4 h-4" />
|
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>
|
</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,69 +1,101 @@
|
|||||||
import React from "react";
|
"use client";
|
||||||
import { ChevronRight, Clock, Calendar, Bookmark, Share2, User } from "lucide-react";
|
|
||||||
|
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 (
|
return (
|
||||||
<header className="mb-16">
|
<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">
|
<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>
|
</a>
|
||||||
|
|
||||||
{/* Meta Info */}
|
|
||||||
<div className="flex flex-wrap items-center gap-4 mb-6 text-xs text-gray-400">
|
<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">
|
<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>
|
||||||
<div className="flex items-center">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
<h1 className="mb-6 text-4xl font-bold leading-tight md:text-5xl">{translation.title}</h1>
|
||||||
{/* Title & Excerpt */}
|
<div className={`mb-8 border-orange-500 ${isRtl ? "pr-4 border-r-2" : "pl-4 border-l-2"}`}>
|
||||||
<h1 className="mb-6 text-4xl font-bold leading-tight md:text-5xl">دیسکلکولیا: وقتی مغز با اعداد کنار نمیآید</h1>
|
<p className="text-lg leading-relaxed text-gray-300">{translation.description}</p>
|
||||||
<div className="pr-4 mb-8 border-r-2 border-orange-500">
|
|
||||||
<p className="text-lg leading-relaxed text-gray-300">
|
|
||||||
پژوهش جدید با تصویربرداری مغزی نشان میدهد مشکل اصلی کودکان مبتلا به اختلال یادگیری ریاضی، نه در درک مقدار، بلکه در پردازش نمادهای عددی
|
|
||||||
نهفته است.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Author & Actions */}
|
|
||||||
<div className="flex items-center justify-between mb-12">
|
<div className="flex items-center justify-between mb-12">
|
||||||
<div className="flex items-center gap-3">
|
<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">
|
<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" />
|
<User className="w-5 h-5" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">تیم آکادمی رابین</div>
|
<div className="text-sm font-medium">{blog.writer.username}</div>
|
||||||
<div className="text-xs text-gray-400">علم و فناوری</div>
|
<div className="text-xs text-gray-400">{t("authorRole")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<Share2 className="w-4 h-4" />
|
||||||
</button>
|
</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">
|
<button
|
||||||
<Bookmark className="w-4 h-4" />
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{blog.featuredImage && (
|
||||||
{/* Hero Image */}
|
<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">
|
||||||
<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">
|
<Image src={`${BACKEND_URL}/uploads/${blog.featuredImage}`} alt={translation.title} fill className="object-cover w-full h-full" />
|
||||||
{/* 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>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,53 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { BookOpen, Clock } from "lucide-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() {
|
export default function RelatedArticles({ data }: { data: Blog[] }) {
|
||||||
const articles = [
|
const t = useTranslations("academy.single");
|
||||||
{
|
const locale = useLocale();
|
||||||
id: 1,
|
const isRtl = locale === "fa" || locale === "ar";
|
||||||
category: "امنیت",
|
|
||||||
title: "۱۰ نکته طلایی برای امنسازی Firewall",
|
if (!data || data.length === 0) return null;
|
||||||
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",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="pt-16 mb-16 border-t border-slate-800">
|
<section className="pt-16 mb-16 border-t border-slate-800">
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex items-center justify-between mb-8">
|
||||||
<h2 className="flex items-center gap-2 text-xl font-bold text-white">
|
<h2 className="flex items-center gap-2 text-xl font-bold text-white">
|
||||||
<BookOpen className="w-5 h-5 text-orange-500" />
|
<BookOpen className="w-5 h-5 text-orange-500" />
|
||||||
مقالات مرتبط
|
{t("relatedArticles")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
{articles.map((article) => (
|
{data.map((b) => {
|
||||||
<div
|
const translation = b.translations.find((t) => t.language === locale) || b.translations[0];
|
||||||
key={article.id}
|
|
||||||
|
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"
|
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">
|
<div className="p-5">
|
||||||
<span className="block mb-3 text-xs font-medium text-orange-500">{article.category}</span>
|
<span className="flex gap-1 mb-3 text-xs font-medium text-orange-500">
|
||||||
<h3 className="mb-4 font-medium text-white transition-colors group-hover:text-orange-400 line-clamp-2">{article.title}</h3>
|
<Clock className={`w-3.5 h-3.5 ${isRtl ? "ml-1" : "mr-1"}`} />
|
||||||
<div className="flex items-center text-xs text-gray-500">
|
{calculateReadingTime(translation)} {t("readTime")}
|
||||||
<Clock className="w-3.5 h-3.5 ml-1" />
|
</span>
|
||||||
{article.time}
|
<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>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Projects({ data }: { data: Portfolio[] }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="flex flex-wrap items-end justify-between gap-4 mx-auto mb-8 sm:mb-12 max-w-7xl">
|
||||||
<div className="text-start">
|
<div className="text-start">
|
||||||
<h2 className="mb-2 text-2xl font-bold sm:text-3xl">{t("sectionTitle")}</h2>
|
<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"
|
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"
|
aria-label="Scroll previous"
|
||||||
>
|
>
|
||||||
{isRTL ? "<" : "<"}
|
{"<"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => scroll("next")}
|
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"
|
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"
|
aria-label="Scroll next"
|
||||||
>
|
>
|
||||||
{isRTL ? ">" : ">"}
|
{">"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
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
|
<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]"
|
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">
|
<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">
|
<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">
|
<button className="text-xs transition-colors sm:text-sm text-accent hover:text-accent/80">
|
||||||
{t("viewDetails")} {isRTL ? ">" : ">"}
|
{t("viewDetails")} {">"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import { useLocale, useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import HeroButtons from "./HeroButtons";
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
const t = useTranslations("software.hero");
|
const t = useTranslations("software.hero");
|
||||||
const locale = useLocale();
|
|
||||||
const isRtl = locale === "fa" || locale === "ar";
|
|
||||||
|
|
||||||
const ArrowIcon = isRtl ? ChevronLeft : ChevronRight;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="px-4 py-12 text-center sm:py-16 md:py-20">
|
<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>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col justify-center gap-3 px-4 sm:flex-row sm:gap-4">
|
<HeroButtons />
|
||||||
<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>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
30
src/components/software/hero/HeroButtons.tsx
Normal file
30
src/components/software/hero/HeroButtons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,74 @@
|
|||||||
"logo1": "رابين",
|
"logo1": "رابين",
|
||||||
"logo2": "شبكة"
|
"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": {
|
"software": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "⚙ تطوير برمجيات مخصصة",
|
"badge": "⚙ تطوير برمجيات مخصصة",
|
||||||
|
|||||||
@@ -7,6 +7,74 @@
|
|||||||
"logo1": "Robin",
|
"logo1": "Robin",
|
||||||
"logo2": "Network"
|
"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": {
|
"software": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "⚙ Custom Software Development",
|
"badge": "⚙ Custom Software Development",
|
||||||
|
|||||||
@@ -7,6 +7,74 @@
|
|||||||
"logo1": "رابین",
|
"logo1": "رابین",
|
||||||
"logo2": "شبکه"
|
"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": {
|
"software": {
|
||||||
"hero": {
|
"hero": {
|
||||||
"badge": "⚙ توسعه نرمافزار سفارشی",
|
"badge": "⚙ توسعه نرمافزار سفارشی",
|
||||||
|
|||||||
15
src/utilities/lib/calculate-reading-time.ts
Normal file
15
src/utilities/lib/calculate-reading-time.ts
Normal 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));
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { formatToPersianDate } from "./format-persian-date";
|
import { formatToPersianDate } from "./format-persian-date";
|
||||||
|
|
||||||
export const formatDateByLocale = (dateString: string, locale: string) => {
|
export const formatDateByLocale = (dateString: string, locale: string) => {
|
||||||
|
console.log(dateString);
|
||||||
if (locale === "fa") {
|
if (locale === "fa") {
|
||||||
return formatToPersianDate(new Date(dateString));
|
return formatToPersianDate(new Date(dateString));
|
||||||
}
|
}
|
||||||
@@ -14,7 +15,6 @@ export const formatDateByLocale = (dateString: string, locale: string) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// برای انگلیسی و سایر زبانها
|
|
||||||
return new Date(dateString).toLocaleDateString(locale, {
|
return new Date(dateString).toLocaleDateString(locale, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
|
|||||||
@@ -1,23 +1,50 @@
|
|||||||
export interface Blog {
|
export interface Blog {
|
||||||
id: string;
|
id: string;
|
||||||
writer: string;
|
|
||||||
featuredImage: string;
|
featuredImage: string;
|
||||||
gallery: string[];
|
gallery: string[];
|
||||||
href: string;
|
href: string;
|
||||||
publishedAt: Date;
|
publishedAt: string;
|
||||||
createdAt: Date;
|
createdAt: string;
|
||||||
updatedAt: Date;
|
translations: BlogTranslation[];
|
||||||
|
writer: {
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlogTranslation {
|
||||||
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: 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 HeadingBlock {
|
||||||
|
|
||||||
export interface EditorDataType {
|
|
||||||
type: EditorKeyDataType;
|
|
||||||
content: any;
|
|
||||||
sort: number;
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user