Files

684 lines
23 KiB
TypeScript

"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",
category: "ویژه: نوآوری",
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 };