Add /products (list + detail) and /blog (list + detail) routes backed by the real Meshkee API, a real category-hierarchy mega menu (level 2/3 only), debounced instant search proxied through a same-origin API route, and tel:-link fallbacks on product cards when no price is set. Also fixes Product price normalization to read the real API shape instead of a guessed one, and hardens product linking against catalog sources that don't expose a slug. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
import { Placeholder } from "@/components/ui/Placeholder";
|
|
import { EmptyState } from "@/components/ui/EmptyState";
|
|
import { getBlogs } from "@/lib/meshkee";
|
|
|
|
const PAGE_SIZE = 12;
|
|
|
|
function formatDate(iso?: string) {
|
|
if (!iso) return "";
|
|
return new Intl.DateTimeFormat("fa-IR", { dateStyle: "long" }).format(new Date(iso));
|
|
}
|
|
|
|
export default async function BlogListPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ page?: string }>;
|
|
}) {
|
|
const params = await searchParams;
|
|
const page = Math.max(1, Number(params.page ?? 1) || 1);
|
|
const { items, total } = await getBlogs({ page, pageSize: PAGE_SIZE });
|
|
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
|
|
|
|
return (
|
|
<div className="mx-auto max-w-7xl px-4 py-10">
|
|
<h1 className="mb-6 text-2xl font-bold text-red-dark">وبلاگ</h1>
|
|
|
|
{items.length === 0 ? (
|
|
<EmptyState message="هنوز مطلبی ثبت نشده است." />
|
|
) : (
|
|
<>
|
|
<div className="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
|
{items.map((post) => (
|
|
<a
|
|
key={post.id}
|
|
href={`/blog/${post.slug}`}
|
|
className="group overflow-hidden rounded-xl border border-border bg-surface transition-shadow hover:shadow-md"
|
|
>
|
|
<Placeholder
|
|
src={post.image ?? "/images/placeholder.svg"}
|
|
alt={post.title}
|
|
width="100%"
|
|
height="180px"
|
|
className="w-full rounded-none border-0"
|
|
/>
|
|
<div className="p-4">
|
|
<div className="mb-2 flex items-center gap-2 text-[11px] text-muted">
|
|
{post.categoryName && <span>{post.categoryName}</span>}
|
|
{post.publishedAt && <span>{formatDate(post.publishedAt)}</span>}
|
|
</div>
|
|
<h3 className="mb-2 line-clamp-2 text-sm font-bold leading-relaxed">
|
|
{post.title}
|
|
</h3>
|
|
{post.abstract && (
|
|
<p className="line-clamp-2 text-xs leading-relaxed text-muted">
|
|
{post.abstract}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</a>
|
|
))}
|
|
</div>
|
|
|
|
{totalPages > 1 && (
|
|
<div className="mt-8 flex items-center justify-center gap-3">
|
|
{page > 1 ? (
|
|
<a
|
|
href={`/blog?page=${page - 1}`}
|
|
className="rounded-lg border border-border px-4 py-2 text-sm text-muted transition-colors hover:border-red hover:text-red"
|
|
>
|
|
قبلی
|
|
</a>
|
|
) : (
|
|
<span className="rounded-lg border border-border px-4 py-2 text-sm text-muted/40">
|
|
قبلی
|
|
</span>
|
|
)}
|
|
<span className="text-sm text-muted">
|
|
صفحه {page} از {totalPages}
|
|
</span>
|
|
{page < totalPages ? (
|
|
<a
|
|
href={`/blog?page=${page + 1}`}
|
|
className="rounded-lg border border-border px-4 py-2 text-sm text-muted transition-colors hover:border-red hover:text-red"
|
|
>
|
|
بعدی
|
|
</a>
|
|
) : (
|
|
<span className="rounded-lg border border-border px-4 py-2 text-sm text-muted/40">
|
|
بعدی
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|