Build the Salar Store storefront and connect products, blogs, and home specials to the Meshkee API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-16 01:36:29 +03:30
co-authored by Cursor
parent 2783305f07
commit 7d26eaa838
113 changed files with 3269 additions and 96 deletions
+57
View File
@@ -0,0 +1,57 @@
import type { Metadata } from "next";
import Image from "next/image";
import { PageHero } from "@/components/PageHero";
import { SITE } from "@/lib/site";
export const metadata: Metadata = {
title: `درباره ما | ${SITE.nameFa}`,
description: SITE.about,
};
export default function AboutPage() {
return (
<>
<PageHero title="درباره ما" english="About Us" />
<section className="container-site py-10 md:py-14">
<div className="relative overflow-hidden rounded-2xl min-h-[280px] md:min-h-[360px]">
<Image
src="/images/pages/about-bg.webp"
alt=""
fill
priority
sizes="1200px"
className="object-cover object-center"
/>
<div
className="pointer-events-none absolute inset-0"
style={{
background:
"linear-gradient(to left, rgba(0,0,0,0.82) 0%, rgba(0,0,0,0.55) 48%, rgba(0,0,0,0.2) 100%)",
}}
/>
<div className="relative z-[1] max-w-xl p-6 md:p-10 text-white">
<h2 className="text-2xl md:text-4xl font-extrabold mb-4">{SITE.nameFa}</h2>
<p className="text-sm md:text-base leading-8 text-white/90 text-justify">{SITE.about}</p>
</div>
</div>
<div className="mt-10 grid md:grid-cols-2 gap-8 items-center rounded-2xl bg-zinc-50 p-5 md:p-8">
<div>
<p className="font-carter text-brand tracking-[0.2em] text-sm mb-2">SALAR STORE</p>
<h2 className="section-title mb-4">سالار استور</h2>
<p className="text-zinc-600 leading-8 text-justify">{SITE.about}</p>
</div>
<div className="relative mx-auto w-full max-w-xs">
<Image
src="/images/pages/about-side.png"
alt={SITE.nameFa}
width={300}
height={603}
className="h-auto w-full object-contain"
/>
</div>
</div>
</section>
</>
);
}
+12
View File
@@ -0,0 +1,12 @@
import { API } from "@/lib/site";
export async function POST(request: Request) {
const body = await request.json();
const response = await fetch(`${API.base}/tenants/${API.domain}/comments`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await response.json();
return Response.json(data, { status: response.status });
}
+11
View File
@@ -0,0 +1,11 @@
import { listMappedProducts } from "@/lib/mappers";
export async function GET(request: Request) {
const query = new URL(request.url).searchParams.get("q")?.trim() ?? "";
if (query.length < 2) {
return Response.json({ items: [] });
}
const data = await listMappedProducts({ name: query, page: 1, pageSize: 8 });
return Response.json({ items: data.items, total: data.total });
}
+63
View File
@@ -0,0 +1,63 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { Media } from "@/components/Media";
import { PageHero } from "@/components/PageHero";
import { RichContent } from "@/components/RichContent";
import { getBlog } from "@/lib/api";
import { formatDate } from "@/lib/format";
import { PLACEHOLDER } from "@/lib/data";
import { SITE } from "@/lib/site";
type Props = {
params: Promise<{ slug: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
try {
const blog = await getBlog(slug);
if (!blog) return { title: SITE.nameFa };
return {
title: `${blog.title} | ${SITE.nameFa}`,
description: blog.abstract || SITE.aboutShort,
};
} catch {
return { title: SITE.nameFa };
}
}
export default async function BlogDetailPage({ params }: Props) {
const { slug } = await params;
const blog = await getBlog(slug);
if (!blog) notFound();
const author = [blog.author?.firstName, blog.author?.lastName].filter(Boolean).join(" ");
return (
<>
<PageHero
title={blog.title}
english="Blog"
crumbs={[{ name: "وبلاگ", href: "/blog" }, { name: blog.title }]}
/>
<article className="container-site py-10 md:py-14">
<Media
src={blog.titleImageUrl || PLACEHOLDER.blog}
alt={blog.title}
className="aspect-[16/7] rounded-2xl"
sizes="1200px"
priority
/>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-zinc-400">
{blog.publishedAt || blog.createdAt ? <time>{formatDate(blog.publishedAt || blog.createdAt || "")}</time> : null}
{blog.categoryName ? <span>{blog.categoryName}</span> : null}
{author ? <span>{author}</span> : null}
</div>
{blog.abstract ? <p className="mt-4 text-zinc-600 leading-8">{blog.abstract}</p> : null}
<div className="mt-8">
<RichContent html={blog.mainTextHtml} />
</div>
</article>
</>
);
}
+65
View File
@@ -0,0 +1,65 @@
import type { Metadata } from "next";
import Link from "next/link";
import { Media } from "@/components/Media";
import { PageHero } from "@/components/PageHero";
import { Pagination } from "@/components/Pagination";
import { getBlogs } from "@/lib/api";
import { mapBlog } from "@/lib/mappers";
import { SITE } from "@/lib/site";
export const metadata: Metadata = {
title: `وبلاگ | ${SITE.nameFa}`,
description: "آخرین مقالات سالار استور",
};
type Props = {
searchParams: Promise<{ page?: string }>;
};
export default async function BlogPage({ searchParams }: Props) {
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
let data: Awaited<ReturnType<typeof getBlogs>> = { items: [], total: 0, page, pageSize: 12 };
try {
data = await getBlogs({ page, pageSize: 12 });
} catch {
data = { items: [], total: 0, page, pageSize: 12 };
}
const posts = data.items.map(mapBlog);
return (
<>
<PageHero title="وبلاگ سالار استور" english="Blog" />
<section className="container-site py-10 md:py-14">
{posts.length ? (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<Link
key={post.slug}
href={`/blog/${post.slug}`}
className="overflow-hidden rounded-2xl border border-black/5 bg-white hover:border-brand/40"
>
<Media src={post.image} alt="" className="aspect-[16/10]" sizes="400px" />
<div className="p-4">
<time className="text-xs text-zinc-400">{post.date}</time>
<h2 className="mt-2 text-base font-bold leading-8 line-clamp-2">{post.title}</h2>
{post.abstract ? (
<p className="mt-1 text-sm leading-7 text-zinc-500 line-clamp-2">{post.abstract}</p>
) : null}
</div>
</Link>
))}
</div>
) : (
<p className="py-16 text-center text-sm text-zinc-500">مقاله‌ای یافت نشد.</p>
)}
<Pagination
page={data.page}
pageSize={data.pageSize}
total={data.total}
makeHref={(nextPage) => (nextPage > 1 ? `/blog?page=${nextPage}` : "/blog")}
/>
</section>
</>
);
}
+94
View File
@@ -0,0 +1,94 @@
import type { Metadata } from "next";
import { ContactForm } from "@/components/ContactForm";
import { PageHero } from "@/components/PageHero";
import { IconInstagram, IconPhone, IconPin, IconTelegram, IconWhatsapp } from "@/components/Icons";
import { SITE } from "@/lib/site";
export const metadata: Metadata = {
title: `تماس با ما | ${SITE.nameFa}`,
description: SITE.address,
};
const contacts = [
{
href: `tel:${SITE.phone}`,
label: SITE.phoneDisplay,
dir: "ltr" as const,
numeric: true,
icon: IconPhone,
},
{
href: SITE.whatsapp,
label: SITE.phoneDisplay,
dir: "ltr" as const,
numeric: true,
icon: IconWhatsapp,
},
{
href: SITE.instagram,
label: SITE.instagramHandle,
dir: "ltr" as const,
numeric: false,
icon: IconInstagram,
},
{
href: SITE.telegram,
label: SITE.telegramHandle,
dir: "ltr" as const,
numeric: false,
icon: IconTelegram,
},
{
href: undefined,
label: SITE.address,
dir: undefined,
numeric: false,
icon: IconPin,
},
];
export default function ContactPage() {
return (
<>
<PageHero title="تماس با ما" english="Contact Us" />
<section className="container-site py-10 md:py-14">
<div className="grid gap-8 lg:grid-cols-[280px_1fr] items-start">
<div className="rounded-2xl border border-black/5 divide-y divide-black/5">
{contacts.map((item) => {
const Icon = item.icon;
const content = (
<>
<span className="grid place-items-center size-10 rounded-full bg-zinc-50 text-brand shrink-0">
<Icon className="w-5 h-5" />
</span>
<span dir={item.dir} className={item.numeric ? "font-oswald tracking-wide" : ""}>
{item.label}
</span>
</>
);
if (!item.href) {
return (
<div key={item.label} className="flex items-center gap-3 px-4 py-4 text-sm">
{content}
</div>
);
}
return (
<a
key={item.href + item.label}
href={item.href}
className="flex items-center gap-3 px-4 py-4 text-sm hover:text-brand"
>
{content}
</a>
);
})}
</div>
<ContactForm />
</div>
</section>
</>
);
}
+251 -17
View File
@@ -1,26 +1,260 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
--color-background: #ffffff;
--color-foreground: #292d32;
--color-brand: #fc2424;
--color-brand-soft: #ffecec;
--color-ink: #292d32;
--color-muted: #f5f6f9;
--font-sans: var(--font-vazir), Tahoma, sans-serif;
--font-oswald: var(--font-oswald), Impact, sans-serif;
--font-carter: var(--font-carter-one), Impact, sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
html {
scroll-behavior: smooth;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
background: #ffffff;
color: var(--color-ink);
font-family: var(--font-sans);
}
.font-oswald {
font-family: var(--font-oswald);
}
.font-carter {
font-family: var(--font-carter);
}
.container-site {
width: min(1200px, calc(100% - 2rem));
margin-inline: auto;
}
.section-title {
font-size: 1.25rem;
font-weight: 800;
color: var(--color-ink);
}
.page-hero {
background-color: #fbfbfc;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3E%3Ccircle cx='2' cy='2' r='1' fill='%23292d32' fill-opacity='0.06'/%3E%3C/svg%3E");
background-size: 28px 28px;
}
::selection {
background: #fc2424;
color: #fff;
}
.footer-bottom-bar {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: space-between;
}
.footer-bottom-site {
color: #9c9c9c;
font-size: 12px;
}
.meshkee-credit {
align-items: center;
color: #9c9c9c;
display: inline-flex;
gap: 10px;
padding: 6px 10px;
position: relative;
text-decoration: none;
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover {
color: #e0e0e0;
text-decoration: none;
}
.meshkee-credit__frame {
inset: 0;
pointer-events: none;
position: absolute;
}
.meshkee-credit__line {
background: #777;
position: absolute;
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit__line--l {
height: 100%;
left: 0;
top: 0;
transform: scaleY(0);
transform-origin: top;
transition-delay: 0s;
width: 1px;
}
.meshkee-credit__line--b {
bottom: 0;
height: 1px;
left: 0;
transform: scaleX(0);
transform-origin: left;
transition-delay: 0.14s;
width: 100%;
}
.meshkee-credit__line--r {
height: 100%;
right: 0;
top: 0;
transform: scaleY(0);
transform-origin: bottom;
transition-delay: 0.28s;
width: 1px;
}
.meshkee-credit__line--t {
height: 1px;
left: 0;
top: 0;
transform: scaleX(0);
transform-origin: right;
transition-delay: 0.42s;
width: 100%;
}
.meshkee-credit:hover .meshkee-credit__line--l,
.meshkee-credit:hover .meshkee-credit__line--r {
transform: scaleY(1);
}
.meshkee-credit:hover .meshkee-credit__line--b,
.meshkee-credit:hover .meshkee-credit__line--t {
transform: scaleX(1);
}
.meshkee-credit:not(:hover) .meshkee-credit__line--l {
transition-delay: 0.42s;
}
.meshkee-credit:not(:hover) .meshkee-credit__line--b {
transition-delay: 0.28s;
}
.meshkee-credit:not(:hover) .meshkee-credit__line--r {
transition-delay: 0.14s;
}
.meshkee-credit:not(:hover) .meshkee-credit__line--t {
transition-delay: 0s;
}
.meshkee-credit__logo {
aspect-ratio: 1 / 1;
display: block;
flex-shrink: 0;
height: 42px;
mix-blend-mode: screen;
object-fit: contain;
opacity: 0.7;
position: relative;
width: auto;
transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover .meshkee-credit__logo {
opacity: 1;
}
.meshkee-credit__text {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 1.25;
min-height: 42px;
position: relative;
}
.meshkee-credit__label {
font-size: 11px;
}
.meshkee-credit__name {
color: #cfcfcf;
font-size: 13px;
font-weight: 600;
transition: color 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}
.meshkee-credit:hover .meshkee-credit__name {
color: #fff;
}
@media (max-width: 575px) {
.footer-bottom-bar {
flex-direction: column;
justify-content: center;
text-align: center;
}
}
.draw-frame {
position: absolute;
inset: 0;
z-index: 5;
pointer-events: none;
border-radius: inherit;
border: 1.5px solid currentColor;
opacity: 0;
transition: opacity 0.25s ease;
}
.draw-frame-host:hover .draw-frame {
opacity: 1;
}
.rich-content {
color: #444;
line-height: 2;
}
.rich-content h2,
.rich-content h3,
.rich-content h4 {
color: var(--color-ink);
font-weight: 800;
margin: 1.5rem 0 0.75rem;
}
.rich-content p,
.rich-content li {
margin-bottom: 0.85rem;
}
.rich-content ul,
.rich-content ol {
padding-inline-start: 1.25rem;
margin-bottom: 1rem;
}
.rich-content a {
color: var(--color-brand);
}
.rich-content img {
border-radius: 12px;
display: block;
height: auto;
margin: 1.25rem auto;
max-width: 100%;
}
+28 -13
View File
@@ -1,29 +1,44 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Carter_One, Oswald, Vazirmatn } from "next/font/google";
import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
import { TopHeader } from "@/components/TopHeader";
import { SITE } from "@/lib/site";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
const vazir = Vazirmatn({
subsets: ["arabic", "latin"],
variable: "--font-vazir",
display: "swap",
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
const oswald = Oswald({
subsets: ["latin"],
variable: "--font-oswald",
display: "swap",
});
const carter = Carter_One({
weight: "400",
subsets: ["latin"],
variable: "--font-carter-one",
display: "swap",
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: `${SITE.nameFa} | ${SITE.tagline}`,
description: SITE.aboutShort,
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html lang="fa" dir="rtl" className={`${vazir.variable} ${oswald.variable} ${carter.variable} h-full antialiased`}>
<body className="min-h-full flex flex-col bg-white text-ink">
<TopHeader />
<Header />
<main className="flex-1">{children}</main>
<Footer />
</body>
</html>
);
}
+13
View File
@@ -0,0 +1,13 @@
import Link from "next/link";
export default function NotFound() {
return (
<section className="container-site py-24 text-center">
<h1 className="text-3xl font-extrabold">صفحه پیدا نشد</h1>
<p className="mt-3 text-zinc-500">محتوای مورد نظر در دسترس نیست.</p>
<Link href="/" className="mt-6 inline-block text-brand">
بازگشت به خانه
</Link>
</section>
);
}
+19 -64
View File
@@ -1,69 +1,24 @@
import Image from "next/image";
import { AboutUs } from "@/components/AboutUs";
import { BannerGrid4, BannerRow3 } from "@/components/Banners";
import { BlogCarousel } from "@/components/BlogCarousel";
import { Brands } from "@/components/Brands";
import { Categories } from "@/components/Categories";
import { HeroSlider } from "@/components/HeroSlider";
import { SpecialSale } from "@/components/SpecialSale";
import { TopSelling } from "@/components/TopSelling";
export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert h-5 w-[100px]"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
<>
<HeroSlider />
<SpecialSale />
<Categories />
<BannerGrid4 />
<TopSelling />
<BannerRow3 />
<Brands />
<AboutUs />
<BlogCarousel />
</>
);
}
+111
View File
@@ -0,0 +1,111 @@
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { PageHero } from "@/components/PageHero";
import { Pagination } from "@/components/Pagination";
import { ProductGrid } from "@/components/ProductGrid";
import { ProductTabs } from "@/components/ProductTabs";
import { ProductView } from "@/components/ProductView";
import { getCategories, getCategoryBySlug, getComments, getProduct, getStoreItemByProduct } from "@/lib/api";
import { listMappedProductsForCategory } from "@/lib/mappers";
import { SITE } from "@/lib/site";
type Props = {
params: Promise<{ slug: string }>;
searchParams: Promise<{ page?: string }>;
};
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
try {
const product = await getProduct(slug);
if (product) {
return {
title: `${product.nameFa || product.title} | ${SITE.nameFa}`,
description: product.summary || SITE.tagline,
};
}
const category = await getCategoryBySlug(slug);
if (category) {
return {
title: `${category.nameFa || category.name} | ${SITE.nameFa}`,
description: SITE.tagline,
};
}
} catch {
// fall through
}
return { title: SITE.nameFa };
}
export default async function ProductOrCategoryPage({ params, searchParams }: Props) {
const { slug } = await params;
const product = await getProduct(slug);
if (product) {
const [storeItem, comments] = await Promise.all([
getStoreItemByProduct(product.id),
getComments("product", product.id),
]);
return (
<>
<PageHero
title={product.nameFa || product.title}
english={product.title}
crumbs={[{ name: "محصولات", href: "/products" }, { name: product.nameFa || product.title }]}
/>
<section className="container-site py-10 md:py-14">
<ProductView product={product} variants={storeItem?.variants ?? []} />
<ProductTabs productId={product.id} descriptionHtml={product.descriptionHtml} comments={comments} />
</section>
</>
);
}
const [category, categories] = await Promise.all([getCategoryBySlug(slug), getCategories()]);
if (!category) notFound();
const { page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const children = categories.filter((item) => item.parentId === category.id);
const data = await listMappedProductsForCategory(
category.id,
children.map((item) => item.id),
page,
16,
);
return (
<>
<PageHero
title={category.nameFa || category.name}
english={category.name}
crumbs={[{ name: "محصولات", href: "/products" }, { name: category.nameFa || category.name }]}
/>
<section className="container-site py-10 md:py-14">
{children.length > 0 && (
<div className="mb-8 flex flex-wrap gap-2">
{children.map((item) => (
<Link
key={item.id}
href={`/products/${item.slug}`}
className="rounded-full border border-black/10 px-4 py-2 text-sm hover:border-brand hover:text-brand"
>
{item.nameFa || item.name}
</Link>
))}
</div>
)}
<ProductGrid products={data.items} />
<Pagination
page={data.page}
pageSize={data.pageSize}
total={data.total}
makeHref={(nextPage) => (nextPage > 1 ? `/products/${slug}?page=${nextPage}` : `/products/${slug}`)}
/>
</section>
</>
);
}
+40
View File
@@ -0,0 +1,40 @@
import type { Metadata } from "next";
import { PageHero } from "@/components/PageHero";
import { Pagination } from "@/components/Pagination";
import { ProductGrid } from "@/components/ProductGrid";
import { listMappedProducts } from "@/lib/mappers";
import { SITE } from "@/lib/site";
export const metadata: Metadata = {
title: `محصولات | ${SITE.nameFa}`,
description: SITE.tagline,
};
type Props = {
searchParams: Promise<{ q?: string; page?: string }>;
};
export default async function ProductsPage({ searchParams }: Props) {
const { q, page: pageParam } = await searchParams;
const page = Math.max(1, Number(pageParam) || 1);
const pageSize = 16;
const data = await listMappedProducts({ page, pageSize, name: q });
const makeHref = (nextPage: number) => {
const params = new URLSearchParams();
if (q) params.set("q", q);
if (nextPage > 1) params.set("page", String(nextPage));
const query = params.toString();
return query ? `/products?${query}` : "/products";
};
return (
<>
<PageHero title={q ? `جستجو: ${q}` : "محصولات"} english="Products" />
<section className="container-site py-10 md:py-14">
<ProductGrid products={data.items} />
<Pagination page={data.page} pageSize={data.pageSize} total={data.total} makeHref={makeHref} />
</section>
</>
);
}