Connect product/blog pages, mega menu, and instant search to Meshkee backend

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>
This commit is contained in:
amirhosein.ashourloo
2026-08-18 13:42:08 +03:30
co-authored by Claude Sonnet 5
parent 1f81236d0a
commit 2b5f4e437b
19 changed files with 1018 additions and 175 deletions
+97
View File
@@ -0,0 +1,97 @@
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>
);
}