Wire forms to the Meshkee contact API, tighten page-title spacing, and polish outline button hover so it fades cleanly. Co-authored-by: Cursor <cursoragent@cursor.com>
228 lines
7.9 KiB
TypeScript
228 lines
7.9 KiB
TypeScript
import type { Metadata } from "next";
|
|
import Link from "next/link";
|
|
import { BlogCoverImage } from "@/components/BlogCoverImage";
|
|
import { ContentPageShell } from "@/components/ContentPageShell";
|
|
import { Pagination } from "@/components/Pagination";
|
|
import {
|
|
findInstructionCategory,
|
|
getDescendantInstructionCategoryIds,
|
|
getRootInstructionCategories,
|
|
instructionCategoryHref,
|
|
instructionCategoryLabelFa,
|
|
topInstructionCategories,
|
|
type InstructionCategory,
|
|
} from "@/data/instructionCategories";
|
|
import {
|
|
getInstructionCategories,
|
|
getInstructionCoverImage,
|
|
getInstructions,
|
|
getInstructionsByCategory,
|
|
} from "@/lib/api";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "آموزشها",
|
|
description: "راهنماها و آموزشهای کاربردی تیم مشکی.",
|
|
alternates: { canonical: "/instruction" },
|
|
};
|
|
|
|
const PAGE_SIZE = 12;
|
|
|
|
type InstructionPageProps = {
|
|
searchParams: Promise<{ page?: string; category?: string }>;
|
|
};
|
|
|
|
function mapApiCategories(
|
|
items: Awaited<ReturnType<typeof getInstructionCategories>>,
|
|
): InstructionCategory[] {
|
|
return items.map((item) => ({
|
|
id: String(item.id),
|
|
slug: item.slug,
|
|
nameEn: item.name,
|
|
nameFa: item.nameFa || item.name,
|
|
parentId: item.parentId,
|
|
sortOrder: item.sortOrder ?? 0,
|
|
}));
|
|
}
|
|
|
|
export default async function InstructionListPage({
|
|
searchParams,
|
|
}: InstructionPageProps) {
|
|
const { page: pageParam, category: categoryParam } = await searchParams;
|
|
const page = Math.max(1, Number(pageParam) || 1);
|
|
|
|
let allCategories: InstructionCategory[] = topInstructionCategories;
|
|
try {
|
|
allCategories = mapApiCategories(await getInstructionCategories());
|
|
} catch {
|
|
allCategories = topInstructionCategories;
|
|
}
|
|
|
|
const rootCategories = getRootInstructionCategories(allCategories);
|
|
const activeCategory =
|
|
findInstructionCategory(categoryParam, allCategories) ??
|
|
findInstructionCategory(categoryParam, rootCategories);
|
|
|
|
let items: Awaited<ReturnType<typeof getInstructions>>["items"] = [];
|
|
let total = 0;
|
|
|
|
try {
|
|
if (activeCategory) {
|
|
const data = await getInstructionsByCategory({
|
|
categoryId: activeCategory.id,
|
|
categoryIds: getDescendantInstructionCategoryIds(
|
|
allCategories,
|
|
activeCategory.id,
|
|
),
|
|
page,
|
|
pageSize: PAGE_SIZE,
|
|
});
|
|
items = data.items;
|
|
total = data.total;
|
|
} else {
|
|
const data = await getInstructions({ page, pageSize: PAGE_SIZE });
|
|
items = data.items;
|
|
total = data.total;
|
|
}
|
|
} catch {
|
|
items = [];
|
|
total = 0;
|
|
}
|
|
|
|
const paginationQuery = activeCategory
|
|
? { category: activeCategory.slug }
|
|
: undefined;
|
|
|
|
const filterCategories =
|
|
rootCategories.length > 0 ? rootCategories : topInstructionCategories;
|
|
|
|
return (
|
|
<ContentPageShell flushTop>
|
|
<section className="pb-20 lg:pb-28">
|
|
<div className="page-title-band mb-8 md:mb-10">
|
|
<div className="relative mx-auto max-w-7xl px-5 pb-9 pt-[calc(var(--header-height)+0.15rem)] lg:px-8 lg:pb-12 lg:pt-[calc(var(--header-height)+0.3rem)]">
|
|
<p
|
|
aria-hidden
|
|
className="latin pointer-events-none absolute inset-x-5 top-[calc(var(--header-height)+0.5rem)] z-0 text-[clamp(1.75rem,5.5vw,3.75rem)] font-light uppercase leading-none tracking-[0.05em] text-ink/[0.07] lg:inset-x-8 lg:top-[calc(var(--header-height)+0.65rem)]"
|
|
>
|
|
Instructions
|
|
</p>
|
|
<div className="reveal relative z-10 max-w-2xl">
|
|
<h1 className="whitespace-normal text-[clamp(1.35rem,3.6vw,2.75rem)] font-normal leading-tight tracking-tight text-ink">
|
|
آموزشها
|
|
</h1>
|
|
<p className="mt-1 text-sm leading-6 text-muted md:text-base">
|
|
راهنماها و آموزشهای کاربردی برای استفاده از خدمات مشکی.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mx-auto max-w-7xl px-5 lg:px-8">
|
|
<div className="reveal mb-10 flex flex-wrap gap-2 md:mb-12">
|
|
<Link
|
|
href="/instruction"
|
|
className={`rounded-full border px-4 py-2 text-sm transition ${
|
|
!activeCategory
|
|
? "border-ink bg-ink text-surface"
|
|
: "border-line/70 text-ink-soft hover:border-ink/30 hover:text-ink"
|
|
}`}
|
|
>
|
|
همه
|
|
</Link>
|
|
{filterCategories.map((category) => {
|
|
const active = activeCategory?.id === category.id;
|
|
return (
|
|
<Link
|
|
key={category.id}
|
|
href={instructionCategoryHref(category.slug)}
|
|
className={`rounded-full border px-4 py-2 text-sm transition ${
|
|
active
|
|
? "border-ink bg-ink text-surface"
|
|
: "border-line/70 text-ink-soft hover:border-ink/30 hover:text-ink"
|
|
}`}
|
|
>
|
|
{category.nameFa}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{items.length === 0 ? (
|
|
<p className="text-sm text-muted">
|
|
آموزشی برای نمایش وجود ندارد.
|
|
</p>
|
|
) : (
|
|
<div
|
|
key={`${activeCategory?.slug ?? "all"}-${page}`}
|
|
className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3"
|
|
>
|
|
{items.map((item, index) => {
|
|
const image = getInstructionCoverImage(item);
|
|
const categoryFa = instructionCategoryLabelFa(
|
|
item.categoryName,
|
|
allCategories,
|
|
);
|
|
const titleFa = item.titleFa || item.title;
|
|
const titleEn =
|
|
item.titleEn ??
|
|
(item.titleFa && item.title !== item.titleFa
|
|
? item.title
|
|
: null);
|
|
|
|
return (
|
|
<article
|
|
key={item.id}
|
|
className={`reveal group overflow-hidden rounded-2xl border border-line/70 ${
|
|
index % 3 === 1
|
|
? "reveal-delay-1"
|
|
: index % 3 === 2
|
|
? "reveal-delay-2"
|
|
: ""
|
|
}`}
|
|
>
|
|
<Link href={`/instruction/${item.slug}`} className="block">
|
|
<div className="relative overflow-hidden bg-paper-deep">
|
|
{image ? (
|
|
<BlogCoverImage src={image} alt={titleFa} />
|
|
) : null}
|
|
</div>
|
|
<div className="px-4 py-4">
|
|
{categoryFa ? (
|
|
<p className="mb-2 text-[11px] text-muted">
|
|
{categoryFa}
|
|
</p>
|
|
) : null}
|
|
<h2 className="group-text-hover line-clamp-2 whitespace-normal text-lg font-normal leading-7 text-ink">
|
|
{titleFa}
|
|
</h2>
|
|
{titleEn ? (
|
|
<p className="latin text-[12px] font-light uppercase tracking-[0.22em] text-ink-soft/70">
|
|
{titleEn}
|
|
</p>
|
|
) : null}
|
|
{item.abstract ? (
|
|
<p className="mt-2 line-clamp-3 text-sm leading-6 text-muted">
|
|
{item.abstract}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</Link>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
<Pagination
|
|
page={page}
|
|
pageSize={PAGE_SIZE}
|
|
total={total}
|
|
basePath="/instruction"
|
|
query={paginationQuery}
|
|
/>
|
|
</div>
|
|
</section>
|
|
</ContentPageShell>
|
|
);
|
|
}
|