596 lines
21 KiB
TypeScript
596 lines
21 KiB
TypeScript
"use client";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { ArrowRight, Slash } from "lucide-react";
|
|
import { Fragment, useCallback, useMemo, useState } from "react";
|
|
import type { ControllerRenderProps } from "react-hook-form";
|
|
import { Controller, useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
|
|
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
|
import {
|
|
Breadcrumb,
|
|
BreadcrumbItem,
|
|
BreadcrumbLink,
|
|
BreadcrumbList,
|
|
BreadcrumbSeparator,
|
|
} from "@/components/ui/breadcrumb";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Field } from "@/components/ui/field";
|
|
import { Label } from "@/components/ui/label";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
interface BreadcrumbItem {
|
|
label: string;
|
|
link: string;
|
|
}
|
|
|
|
interface Post {
|
|
categoryKey: string;
|
|
category: string;
|
|
title: string;
|
|
summary: string;
|
|
link: string;
|
|
cta: string;
|
|
thumbnail: string;
|
|
}
|
|
|
|
interface Category {
|
|
label: string;
|
|
value: string;
|
|
}
|
|
|
|
interface FilterFormProps {
|
|
categories: Array<Category>;
|
|
onCategoryChange: (selectedCategories: string[]) => void;
|
|
}
|
|
|
|
interface BlogsResultProps {
|
|
posts: Array<Post>;
|
|
categories: Array<Category>;
|
|
}
|
|
|
|
interface BreadcrumbBlogProps {
|
|
breadcrumb: Array<BreadcrumbItem>;
|
|
}
|
|
|
|
const POSTS_PER_PAGE = 6;
|
|
|
|
const BREADCRUMB: Array<BreadcrumbItem> = [
|
|
{
|
|
label: "منابع",
|
|
link: "#",
|
|
},
|
|
{
|
|
label: "وبلاگ",
|
|
link: "#",
|
|
},
|
|
];
|
|
|
|
const CATEGORIES: Array<Category> = [
|
|
{
|
|
label: "همه",
|
|
value: "all",
|
|
},
|
|
{
|
|
label: "بهرهوری",
|
|
value: "productivity",
|
|
},
|
|
{
|
|
label: "دسترسپذیری",
|
|
value: "accessibility",
|
|
},
|
|
{
|
|
label: "کارایی",
|
|
value: "performance",
|
|
},
|
|
];
|
|
|
|
const PRIMARY_POST: Post = {
|
|
categoryKey: "all",
|
|
category: "ویژه نوآوری",
|
|
title: "چگونه هوش مصنوعی توسعه فرانتاند را متحول میکند",
|
|
summary:
|
|
"ببینید ابزارهایی مانند GitHub Copilot و دستیارهای هوش مصنوعی چگونه فرایند طراحی رابط کاربری و توسعه نرمافزار را متحول میکنند.",
|
|
link: "#",
|
|
cta: "بیشتر بدانید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-1.svg",
|
|
};
|
|
|
|
const POSTS: Array<Post> = [
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "۵ افزونه ویرایشگر کد که ساعتها وقت شما را ذخیره میکند",
|
|
summary:
|
|
"افزونههای ضروری برای افزایش بهرهوری کدنویسی و سادهتر کردن گردش کار.",
|
|
link: "#",
|
|
cta: "ویرایشگر را تقویت کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-2.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "مدیریت زمان برای توسعهدهندگان: چه چیزی واقعاً جواب میدهد",
|
|
summary:
|
|
"استراتژیهای اثباتشده برای جلوگیری از فرسودگی و مدیریت وظایف بدون استرس.",
|
|
link: "#",
|
|
cta: "زمان را مدیریت کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-3.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "خودکارسازی گردش کار با اجرای خودکار وظایفها",
|
|
summary:
|
|
"از Gulp، اسکریپتهای npm و GitHub Actions برای خودکار کردن کارهای تکراری توسعه استفاده کنید.",
|
|
link: "#",
|
|
cta: "همین حالا خودکار کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-4.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "روتینهای روزانه مؤثر برای توسعهدهندگان",
|
|
summary:
|
|
"روتینهایی که توسعهدهندگان برتر برای ماندن پرانرژی، خلاق و متمرکز دنبال میکنند.",
|
|
link: "#",
|
|
cta: "جریان کاری خود را بیابید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-5.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "تسلط بر کنترل نسخه با این میانبرها",
|
|
summary:
|
|
"با استفاده از Aliasها و ترفندهای Git، مدیریت نسخه و فرایند توسعه را سریعتر و سادهتر کنید.",
|
|
link: "#",
|
|
cta: "Git را سریعتر کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-6.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "کاهش تعویض زمینه در کار توسعه",
|
|
summary:
|
|
"حواسپرتی را کم کنید و با تمرکز عمیق روی کد پیش بروید.",
|
|
link: "#",
|
|
cta: "متمرکز بمانید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-1.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "راهاندازی دورکاری: ابزارهای محیط بدون حواسپرتی",
|
|
summary:
|
|
"فضا و استک نرمافزاری را برای حداکثر بهرهوری در کار از خانه تنظیم کنید.",
|
|
link: "#",
|
|
cta: "محیط کار را ارتقا دهید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-2.svg",
|
|
},
|
|
{
|
|
categoryKey: "productivity",
|
|
category: "بهرهوری",
|
|
title: "پومودورو برای برنامهنویسان: واقعاً مؤثر است؟",
|
|
summary:
|
|
"بررسی عملی تکنیک پومودورو و اثربخشی آن در توسعه نرمافزار.",
|
|
link: "#",
|
|
cta: "روش را امتحان کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-3.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "چرا دسترسپذیری باید بخشی از نسخه اولیه محصول شما باشد",
|
|
summary:
|
|
"فراگیر کردن محصول از روز اول، قابلیت استفاده و دسترسی را بهبود میدهد.",
|
|
link: "#",
|
|
cta: "دلیلش را بدانید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-4.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "استفاده درست از نقشهای برچسب دسترسپذیری در وباپ",
|
|
summary:
|
|
"یاد بگیرید چگونه با نقشها و Landmarkهای ARIA، دسترسیپذیری برای صفحهخوانها را بهبود دهید.",
|
|
link: "#",
|
|
cta: "معناشناسی را بهبود دهید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-5.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "نکات کنتراست رنگ برای خوانایی بهتر",
|
|
summary:
|
|
"ترکیبهای رنگی دسترسپذیر مطابق استاندارد WCAG را بیاموزید.",
|
|
link: "#",
|
|
cta: "رنگها را اصلاح کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-6.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "ناوبری با کیبورد: تجربهای که نادیده گرفته میشود",
|
|
summary:
|
|
"وبسایت را کاملاً با کیبورد قابل استفاده کنید برای دسترسپذیری و سرعت.",
|
|
link: "#",
|
|
cta: "ناوبری را تست کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-1.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "فرمهای دسترسپذیر: برچسب، خطا و بازخورد",
|
|
summary:
|
|
"با دستورالعملهای واضح، فرمهایی طراحی کنید که هم برای کاربران و هم برای صفحهخوانها قابل درک باشند.",
|
|
link: "#",
|
|
cta: "فرمها را اصلاح کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-2.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "تست با صفحهخوان: راهنمای مبتدی",
|
|
summary:
|
|
"یاد بگیرید چگونه وبسایت خود را با صفحهخوانهای رایج آزمایش و مشکلات دسترسپذیری را شناسایی کنید.",
|
|
link: "#",
|
|
cta: "تست را شروع کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-3.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "تفکر طراحی فراگیر در توسعه رابط کاربری",
|
|
summary:
|
|
"رابطهایی طراحی کنید که از ابتدا همه تواناییها را در نظر بگیرند.",
|
|
link: "#",
|
|
cta: "برای همه طراحی کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-4.svg",
|
|
},
|
|
{
|
|
categoryKey: "accessibility",
|
|
category: "دسترسپذیری",
|
|
title: "ممیزی دسترسپذیری: ابزارها و چکلیستها",
|
|
summary:
|
|
"ممیزی کامل با Axe، Lighthouse و چکلیستهای دستی انجام دهید.",
|
|
link: "#",
|
|
cta: "ممیزی کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-5.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "بارگذاری تنبل تصاویر با استاندارد وب مدرن",
|
|
summary:
|
|
"زمان بارگذاری را با lazy-loading بومی و استراتژیهای جایگزین بهبود دهید.",
|
|
link: "#",
|
|
cta: "تصاویر را بهینه کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-6.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "فشردهسازی برنامهنویسی وب بدون ایجاد اختلال در برنامه",
|
|
summary:
|
|
"بهترین روشهای minify و tree-shaking برای bundleهای سریعتر.",
|
|
link: "#",
|
|
cta: "کد را کوچک کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-1.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "شاخصهای عملکرد وب توضیح داده شد: جابجایی چیدمان، زمان بارگذاری اصلی، تأخیر اولین تعامل",
|
|
summary:
|
|
"اندازهگیری و بهبود Core Web Vitals برای تجربه کاربری بهتر.",
|
|
link: "#",
|
|
cta: "شاخصها را بهبود دهید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-2.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "رندر سمت سرور در برابر کلاینت: کدام سریعتر است؟",
|
|
summary:
|
|
"SSR و CSR را مقایسه کنید و بدانید هر کدام چه زمانی برای کارایی بهتر مناسباند.",
|
|
link: "#",
|
|
cta: "گزینهها را بررسی کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-3.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "بهینهسازی فونت برای بارگذاری سریعتر",
|
|
summary:
|
|
"فونتها را بدون مسدود کردن رندر یا ایجاد جابهجایی چیدمان بارگذاری کنید.",
|
|
link: "#",
|
|
cta: "فونتها را سریع کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-4.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "کاهش حجم بسته با تقسیم هوشمند بار",
|
|
summary:
|
|
"با import پویا و chunking مبتنی بر مسیر، زمان بار اولیه را کم کنید.",
|
|
link: "#",
|
|
cta: "تقسیمبندی کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-5.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "استراتژیهای کش برای وباپهای مدرن",
|
|
summary:
|
|
"از کش HTTP، service worker و CDN برای سرعت و پشتیبانی آفلاین بهره ببرید.",
|
|
link: "#",
|
|
cta: "هوشمندانه کش کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-6.svg",
|
|
},
|
|
{
|
|
categoryKey: "performance",
|
|
category: "کارایی",
|
|
title: "تحلیل گلوگاههای کارایی با ابزار تحلیل مرورگر",
|
|
summary:
|
|
"با تب Performance در DevTools مشکلات زمان اجرا را پیدا و رفع کنید.",
|
|
link: "#",
|
|
cta: "همین حالا تحلیل کنید",
|
|
thumbnail:
|
|
"/block-media/shadcnblocks/block/placeholder-1.svg",
|
|
},
|
|
];
|
|
|
|
const FilterFormSchema = z.object({
|
|
items: z.array(z.string()).refine((value) => value.length > 0, {
|
|
message: "حداقل یک دسته باید انتخاب شود.",
|
|
}),
|
|
});
|
|
|
|
const FilterForm = ({ categories, onCategoryChange }: FilterFormProps) => {
|
|
const form = useForm<z.infer<typeof FilterFormSchema>>({
|
|
resolver: zodResolver(FilterFormSchema),
|
|
defaultValues: {
|
|
items: [CATEGORIES[0].value],
|
|
},
|
|
});
|
|
|
|
const handleCheckboxChange = useCallback(
|
|
(
|
|
checked: boolean | string,
|
|
categoryValue: string,
|
|
field: ControllerRenderProps<z.infer<typeof FilterFormSchema>, "items">,
|
|
) => {
|
|
let updatedValues = checked
|
|
? [...field.value, categoryValue]
|
|
: field.value.filter((value: string) => value !== categoryValue);
|
|
|
|
if (updatedValues.length === 0) {
|
|
form.setValue("items", ["all"]);
|
|
onCategoryChange(["all"]);
|
|
return;
|
|
}
|
|
|
|
if (updatedValues.includes("all")) {
|
|
updatedValues = updatedValues.filter((v: string) => v !== "all");
|
|
}
|
|
|
|
if (JSON.stringify(field.value) !== JSON.stringify(updatedValues)) {
|
|
form.setValue("items", updatedValues);
|
|
onCategoryChange(updatedValues);
|
|
}
|
|
},
|
|
[form, onCategoryChange],
|
|
);
|
|
|
|
return (
|
|
<form>
|
|
<Controller
|
|
control={form.control}
|
|
name="items"
|
|
render={({ field }) => (
|
|
<Field className="flex w-full flex-wrap items-center gap-2.5">
|
|
{categories.map((category) => {
|
|
const isChecked = field.value?.includes(category.value);
|
|
return (
|
|
<Field
|
|
key={category.value}
|
|
className="flex flex-row items-start gap-3 space-y-0"
|
|
>
|
|
<Label className="flex cursor-pointer items-center gap-2.5 rounded-full bg-muted px-2.5 py-1.5">
|
|
<div>{category.label}</div>
|
|
<Checkbox
|
|
checked={isChecked}
|
|
onCheckedChange={(checked) =>
|
|
handleCheckboxChange(checked, category.value, field)
|
|
}
|
|
/>
|
|
</Label>
|
|
</Field>
|
|
);
|
|
})}
|
|
</Field>
|
|
)}
|
|
/>
|
|
</form>
|
|
);
|
|
};
|
|
|
|
const BlogsResult = ({ posts, categories }: BlogsResultProps) => {
|
|
const [visibleCount, setVisibleCount] = useState(POSTS_PER_PAGE);
|
|
const [selectedCategories, setSelectedCategories] = useState<string[]>([
|
|
CATEGORIES[0].value,
|
|
]);
|
|
const handleCategoryChange = useCallback((selected: string[]) => {
|
|
setSelectedCategories(selected);
|
|
setVisibleCount(POSTS_PER_PAGE);
|
|
}, []);
|
|
|
|
const handleLoadMore = useCallback(() => {
|
|
setVisibleCount((prev) => prev + POSTS_PER_PAGE);
|
|
}, []);
|
|
const filteredPosts = useMemo(() => {
|
|
return posts.filter(
|
|
(post) =>
|
|
selectedCategories.includes(post.categoryKey) ||
|
|
selectedCategories.includes("all"),
|
|
);
|
|
}, [posts, selectedCategories]);
|
|
|
|
const postsToDisplay = filteredPosts.length > 0 ? filteredPosts : posts;
|
|
|
|
const hasMore = visibleCount < postsToDisplay.length;
|
|
|
|
return (
|
|
<div>
|
|
<FilterForm
|
|
categories={categories}
|
|
onCategoryChange={handleCategoryChange}
|
|
/>
|
|
<div className="flex w-full flex-col gap-4 py-8 lg:gap-8">
|
|
<div className="grid gap-10 md:grid-cols-2 lg:grid-cols-3">
|
|
{postsToDisplay.slice(0, visibleCount).map((post) => (
|
|
<BlogCard key={post.title} {...post} />
|
|
))}
|
|
</div>
|
|
<div className="flex justify-center">
|
|
{hasMore && (
|
|
<Button variant="secondary" onClick={handleLoadMore}>
|
|
بارگذاری بیشتر
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const BreadcrumbBlog = ({ breadcrumb }: BreadcrumbBlogProps) => {
|
|
return (
|
|
<Breadcrumb>
|
|
<BreadcrumbList>
|
|
{breadcrumb.map((item, i) => {
|
|
return (
|
|
<Fragment key={`${item.label}`}>
|
|
<BreadcrumbItem>
|
|
<BreadcrumbLink href={item.link}>{item.label}</BreadcrumbLink>
|
|
</BreadcrumbItem>
|
|
{i < breadcrumb.length - 1 ? (
|
|
<BreadcrumbSeparator>
|
|
<Slash />
|
|
</BreadcrumbSeparator>
|
|
) : null}
|
|
</Fragment>
|
|
);
|
|
})}
|
|
</BreadcrumbList>
|
|
</Breadcrumb>
|
|
);
|
|
};
|
|
|
|
const BlogCard = ({ category, title, thumbnail, summary, link, cta }: Post) => {
|
|
return (
|
|
<a href={link} className="block h-full w-full">
|
|
<Card className="size-full rounded-lg border py-0">
|
|
<CardContent className="p-0">
|
|
<div className="border-b p-2.5 text-start text-sm leading-[1.2] font-medium text-muted-foreground">
|
|
{category}
|
|
</div>
|
|
<AspectRatio ratio={1.520833333} className="overflow-hidden">
|
|
<img
|
|
src={thumbnail}
|
|
alt={title}
|
|
className="block size-full object-cover object-center"
|
|
/>
|
|
</AspectRatio>
|
|
<div className="flex w-full flex-col gap-5 p-5 text-start">
|
|
<h2 className="text-lg leading-tight font-medium md:text-xl">
|
|
{title}
|
|
</h2>
|
|
<div className="w-full max-w-[20rem]">
|
|
<p className="text-sm leading-[1.4] font-medium text-muted-foreground">
|
|
{summary}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<Button size="sm" variant="outline">
|
|
{cta}
|
|
<ArrowRight className="rtl:rotate-180" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</a>
|
|
);
|
|
};
|
|
|
|
interface Blog27Props {
|
|
className?: string;
|
|
}
|
|
|
|
const Blog27 = ({ className }: Blog27Props) => {
|
|
return (
|
|
<section dir="rtl" lang="fa" className={cn("pb-32", className)}>
|
|
<div className="bg-muted bg-[url('/block-media/shadcnblocks/block/patterns/dot-pattern-2.svg')] bg-[length:3.125rem_3.125rem] bg-repeat">
|
|
<div className="container flex flex-col items-start justify-start gap-16 py-20 lg:flex-row lg:items-center lg:justify-between">
|
|
<div className="flex w-full flex-col justify-between gap-12">
|
|
<div className="flex w-full max-w-[36rem] flex-col gap-8 text-start">
|
|
<BreadcrumbBlog breadcrumb={BREADCRUMB} />
|
|
<div className="flex w-full flex-col gap-5">
|
|
<h1 className="text-[2.5rem] leading-[1.2] font-semibold md:text-2xll lg:text-2xll">
|
|
بهترین مقالات وبلاگ
|
|
</h1>
|
|
<p className="text-xl leading-[1.4] text-muted-foreground">
|
|
یک وبلاگ موفق با محتوای ارزشمند، کاربردی و قابل اعتماد، مخاطب را جذب کرده و او را به مطالعه بیشتر ترغیب میکند.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="w-full max-w-[27.5rem]">
|
|
<BlogCard {...PRIMARY_POST} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="py-20">
|
|
<div className="container flex flex-col gap-8 text-start">
|
|
<h2 className="text-[1.75rem] leading-none font-medium md:text-[2.25rem] lg:text-[2rem]">
|
|
همه مطالب
|
|
</h2>
|
|
<div>
|
|
<BlogsResult posts={POSTS} categories={CATEGORIES} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
export { Blog27 };
|