2026-07-11 21:21:29 +03:30
"use client" ;
import { zodResolver } from "@hookform/resolvers/zod" ;
import { ArrowRight , Slash } from "lucide-react" ;
import { Fragment , useCallback , useMemo , useState } from "react" ;
import { ControllerRenderProps , useForm } from "react-hook-form" ;
import { Controller } from "react-hook-form" ;
import { z } from "zod" ;
import { AspectRatio } from "@/components/ui/aspect-ratio" ;
import { Badge } from "@/components/ui/badge" ;
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 ,
FieldError ,
FieldGroup ,
FieldLabel ,
} from "@/components/ui/field" ;
import { Input } from "@/components/ui/input" ;
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" ,
2026-07-15 20:22:14 +02:00
category : "ویژه: نوآوری" ,
2026-07-11 21:21:29 +03:30
title : "چگونه هوش مصنوعی توسعه فرانتاند را متحول میکند" ,
summary :
"ببینید ابزارهایی مثل GitHub Copilot، مولدهای طراحی هوش مصنوعی و دستیارهای کدنویسی چگونه ساخت UI و تحویل قابلیتها را برای توسعهدهندگان تغییر میدهند." ,
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 :
"گردش کار version control را با 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 پشتیبانی screen reader را تقویت کنید." ,
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 :
"با دستورالعملهای واضح برای screen reader و کاربران، فرمها را قابل فهمتر کنید." ,
link : "#" ,
cta : "فرمها را اصلاح کنید" ,
thumbnail :
"/block-media/shadcnblocks/block/placeholder-2.svg" ,
},
{
categoryKey : "accessibility" ,
category : "دسترسپذیری" ,
title : "تست با صفحهخوان: راهنمای مبتدی" ,
summary :
"چگونه با screen readerهای رایج سایت را تست کنید و به چه چیزهایی گوش دهید." ,
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 :
"فونت را بدون مسدود کردن رندر یا جابجایی layout بارگذاری کنید." ,
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 no categories are checked, add "all"
if ( updatedValues . length === 0 ) {
form . setValue ( "items" , [ "all" ]);
onCategoryChange ([ "all" ]);
return ;
}
// Remove "all" if specific category is checked
if ( updatedValues . includes ( "all" )) {
updatedValues = updatedValues . filter (( v : string ) => v !== "all" );
}
// Avoid unnecessary updates
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 }) => (
< FieldGroup className = "flex w-full flex-row flex-wrap gap-2.5" >
{ categories . map (( category ) => {
const isChecked = field . value ? . includes ( category . value );
const inputId = `category- ${ category . value } ` ;
return (
< FieldLabel
key = { category . value }
htmlFor = { inputId }
className = "flex cursor-pointer items-center gap-2.5 rounded-full bg-muted px-2.5 py-1.5"
>
{ category . label }
< Checkbox
id = { inputId }
checked = { isChecked }
onCheckedChange = {( checked ) =>
handleCheckboxChange ( checked , category . value , field )
}
/>
</ FieldLabel >
);
})}
< FieldError
errors = {
form . formState . errors . items
? [ form . formState . errors . items ]
: undefined
}
/>
</ FieldGroup >
)}
/>
</ form >
);
};
const ResourcesResult = ({ 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 ) => (
< ResourcesCard 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 EmailFormSchema = z . object ({
email : z
. string ()
. min ( 1 , "ایمیل الزامی است" )
. email ( "آدرس ایمیل نامعتبر است" ),
});
const EmailForm = () => {
const form = useForm < z.infer < typeof EmailFormSchema >>({
resolver : zodResolver ( EmailFormSchema ),
defaultValues : {
email : "" ,
},
});
function onSubmit ( values : z.infer < typeof EmailFormSchema >) {
console . log ( values );
}
return (
< form onSubmit = { form . handleSubmit ( onSubmit )} className = "w-full" >
< Controller
control = { form . control }
name = "email"
render = {({ field , fieldState }) => (
< Field data-invalid = { fieldState . invalid }>
< FieldLabel htmlFor = "emailInput" className = "sr-only" >
ایمیل
</ FieldLabel >
< div className = "w-full" >
< div className = "relative flex w-full flex-col gap-2 lg:block" >
< Input
{ ...field }
type = "email"
id = "emailInput"
dir = "rtl"
placeholder = "ایمیل خود را وارد کنید"
className = "h-fit bg-background py-4 ps-5 pe-5 text-start lg:pr-[13.75rem]"
aria-invalid = { fieldState . invalid }
/>
< div className = "top-1/2 right-2.5 lg:absolute lg:-translate-y-1/2" >
< Button
type = "submit"
className = "w-full rounded-full lg:w-fit"
>
مشاهده محصول در عمل
< ArrowRight className = "rtl:rotate-180" />
</ Button >
</ div >
</ div >
{ fieldState . invalid && (
< FieldError errors = {[ fieldState . error ]} className = "py-1" />
)}
</ div >
</ Field >
)}
/>
</ form >
);
};
const ResourcesCard = ({
category ,
title ,
thumbnail ,
summary ,
link ,
cta ,
} : Post ) => {
return (
< a href = { link } className = "block h-full w-full" >
< Card className = "size-full 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-none font-bold md:text-xl" >
{ title }
</ h2 >
< div className = "w-full max-w-[20rem]" >
< p className = "text-sm leading-[1.4] font-medium text-foreground" >
{ summary }
</ p >
</ div >
< div >
< Badge className = "rounded-full" >
{ cta }
< ArrowRight className = "rtl:rotate-180" />
</ Badge >
</ div >
</ div >
</ CardContent >
</ Card >
</ a >
);
};
interface Resources1Props {
className? : string ;
}
const Resources1 = ({ className } : Resources1Props ) => {
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] font-semibold text-foreground" >
بهترین گزارش، گزارشی است که خواننده را با محتوای جذاب و
پژوهشمحور، در قالبی روشن و قابلدرک، درگیر نگه میدارد .
</ p >
</ div >
< div className = "max-w-[30rem]" >
< EmailForm />
</ div >
</ div >
</ div >
< div className = "w-full max-w-[27.5rem]" >
< ResourcesCard { ...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 >
< ResourcesResult posts = { POSTS } categories = { CATEGORIES } />
</ div >
</ div >
</ div >
</ section >
);
};
export { Resources1 };