+
+
+
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 43fe0ef..e81eec8 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -100,3 +100,117 @@ h6 {
height: auto;
border-radius: 1rem;
}
+
+/* Rich-text tables arrive as raw CMS HTML. Keep every article table aligned
+ with the full article column while allowing a readable horizontal scroll on
+ narrow screens instead of squeezing several columns into tiny cells. */
+.article-table-scroll {
+ width: 100%;
+ margin-block: 2rem;
+ overflow-x: auto;
+ overscroll-behavior-inline: contain;
+ border: 1px solid var(--border);
+ border-radius: 0.875rem;
+ background: var(--surface);
+ scrollbar-width: thin;
+ scrollbar-color: var(--red-muted) transparent;
+ -webkit-overflow-scrolling: touch;
+}
+
+.article-table-scroll:focus-visible {
+ outline: 2px solid var(--red);
+ outline-offset: 2px;
+}
+
+.article-content table {
+ width: 100% !important;
+ min-width: 600px;
+ margin: 0;
+ border: 0;
+ border-spacing: 0;
+ border-collapse: separate;
+ overflow: hidden;
+ border-radius: 0.875rem;
+ background: var(--surface);
+ font-size: 0.925rem;
+ line-height: 1.8;
+}
+
+.article-content thead {
+ background: var(--red-light);
+}
+
+.article-content th,
+.article-content td {
+ padding: 0.9rem 1rem;
+ text-align: right;
+ vertical-align: middle;
+ border-bottom: 1px solid var(--border);
+ border-left: 1px solid var(--border);
+}
+
+.article-content th {
+ color: var(--red-dark);
+ font-family: var(--font-iranyekan-bold), var(--font-iranyekan), system-ui, sans-serif;
+ font-weight: 700;
+ white-space: nowrap;
+}
+
+.article-content th:last-child,
+.article-content td:last-child {
+ border-left: 0;
+}
+
+.article-content tbody tr:nth-child(even) {
+ background: #fafafa;
+}
+
+.article-content tbody tr {
+ transition: background-color 160ms ease;
+}
+
+.article-content tbody tr:hover {
+ background: var(--red-light);
+}
+
+.article-content tbody tr:last-child td {
+ border-bottom: 0;
+}
+
+@media (max-width: 640px) {
+ .article-content h2 {
+ margin-top: 1.75rem;
+ margin-bottom: 0.75rem;
+ font-size: 1.125rem;
+ line-height: 1.8;
+ }
+
+ .article-content h3 {
+ margin-top: 1.5rem;
+ margin-bottom: 0.625rem;
+ font-size: 1rem;
+ line-height: 1.75;
+ }
+
+ .article-content h4 {
+ margin-top: 1.25rem;
+ margin-bottom: 0.5rem;
+ font-size: 0.925rem;
+ line-height: 1.75;
+ }
+
+ .article-table-scroll {
+ margin-inline: 0;
+ border-radius: 0.75rem;
+ }
+
+ .article-content table {
+ min-width: 560px;
+ font-size: 0.85rem;
+ }
+
+ .article-content th,
+ .article-content td {
+ padding: 0.75rem;
+ }
+}
diff --git a/src/app/products/[slug]/page.tsx b/src/app/products/[slug]/page.tsx
index 2b7ebab..161b458 100644
--- a/src/app/products/[slug]/page.tsx
+++ b/src/app/products/[slug]/page.tsx
@@ -1,17 +1,4 @@
-import { notFound } from "next/navigation";
-import { Breadcrumb } from "@/components/ui/Breadcrumb";
-import { ProductGallery } from "@/components/product/ProductGallery";
-import { ProductPurchasePanel } from "@/components/product/ProductPurchasePanel";
-import { ProductTabs } from "@/components/product/ProductTabs";
-import { SimilarProducts } from "@/components/product/SimilarProducts";
-import {
- getBusinessInfo,
- getComments,
- getProduct,
- getProductStoreVariants,
- getProductTechnicalInfo,
- getProductVariations,
-} from "@/lib/meshkee";
+import { permanentRedirect } from "next/navigation";
export default async function ProductDetailPage({
params,
@@ -19,79 +6,5 @@ export default async function ProductDetailPage({
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
- const [product, technicalInfo, variations, businessInfo] = await Promise.all([
- getProduct(slug),
- getProductTechnicalInfo(slug),
- getProductVariations(slug),
- getBusinessInfo(),
- ]);
-
- if (!product) notFound();
-
- const [storeVariants, comments] = await Promise.all([
- getProductStoreVariants(product.id),
- getComments("product", product.id),
- ]);
- const contactPhone = businessInfo?.phoneNumbers[0];
- const images = product.images.length > 0 ? product.images : product.image ? [product.image] : [];
-
- return (
- >;
}) {
const params = await searchParams;
- const page = Math.max(1, Number(params.page ?? 1) || 1);
+ const query = new URLSearchParams();
- // "مشاهده همه" from a homepage carousel — those are admin-curated,
- // already-complete lists (GET /store-specials has no pagination), so show
- // that exact group's items rather than re-querying the general catalog.
- if (params.specialId) {
- const groups = await getStoreSpecials();
- const group = groups.find((g) => g.id === params.specialId);
- const businessInfo = await getBusinessInfo();
- const contactPhone = businessInfo?.phoneNumbers[0];
- const items = group?.items ?? [];
-
- return (
-
+ {post.categoryName && {post.categoryName}}
+ {post.publishedAt && (
+ <>
+ •
+ {formatDate(post.publishedAt)}
+ >
+ )}
+ {post.author && (
+ <>
+ •
+ {post.author}
+ >
+ )}
+
-
- {post.categoryName && {post.categoryName}}
- {post.publishedAt && (
- <>
- •
- {formatDate(post.publishedAt)}
- >
- )}
- {post.author && (
- <>
- •
- {post.author}
- >
- )}
+
-
- + {post.title} +
+ + + +
+
+ → بازگشت به وبلاگ
+
+
+
+
+ - {post.title} -
- - - - -
-
-
-
-
-
-
- );
+ permanentRedirect(`/store-items/${slug}`);
}
diff --git a/src/app/products/id/[id]/page.tsx b/src/app/products/id/[id]/page.tsx
index cbc0eca..d9491ce 100644
--- a/src/app/products/id/[id]/page.tsx
+++ b/src/app/products/id/[id]/page.tsx
@@ -11,5 +11,5 @@ export default async function ProductByIdPage({
if (!product?.slug) notFound();
- redirect(`/products/${product.slug}`);
+ redirect(`/store-items/${product.slug}`);
}
diff --git a/src/app/products/page.tsx b/src/app/products/page.tsx
index 8910c16..df4434e 100644
--- a/src/app/products/page.tsx
+++ b/src/app/products/page.tsx
@@ -1,176 +1,18 @@
-import { Breadcrumb } from "@/components/ui/Breadcrumb";
-import { ProductCard } from "@/components/ui/ProductCard";
-import { EmptyState } from "@/components/ui/EmptyState";
-import {
- type CategoryTreeGroup,
- getBusinessInfo,
- getCategoryTree,
- getProducts,
- getStoreSpecials,
- productHref,
-} from "@/lib/meshkee";
-
-const PAGE_SIZE = 24;
-
-function findCategoryLabel(tree: CategoryTreeGroup[], id: string): string | undefined {
- for (const group of tree) {
- for (const item of group.items) {
- if (item.id === id) return item.nameFa;
- const child = item.children.find((c) => c.id === id);
- if (child) return child.nameFa;
- }
- }
- return undefined;
-}
+import { permanentRedirect } from "next/navigation";
export default async function ProductsPage({
searchParams,
}: {
- searchParams: Promise<{ categoryId?: string; q?: string; page?: string; specialId?: string }>;
+ searchParams: Promise
-
- {product.brand && (
-
-
-
-
-
-
- - {product.brand} -
- )} -{product.name}
- > - } - variations={variations} - storeVariants={storeVariants} - fallbackPrice={{ - minPrice: product.minPrice, - maxPrice: product.maxPrice, - oldPrice: product.oldPrice, - discount: product.discount, - }} - inStock={product.inStock} - contactPhone={contactPhone} - storeName={businessInfo?.nameFa} - productName={product.name} - productSlug={product.slug} - productImage={product.image ?? images[0] ?? ""} - /> -
-
-
- ) : (
-
- );
+ for (const [key, value] of Object.entries(params)) {
+ if (Array.isArray(value)) value.forEach((item) => query.append(key, item));
+ else if (value != null) query.set(key, value);
}
- const [{ items, total }, businessInfo, categoryTree] = await Promise.all([
- getProducts({
- categoryId: params.categoryId,
- name: params.q,
- page,
- pageSize: PAGE_SIZE,
- }),
- getBusinessInfo(),
- params.categoryId ? getCategoryTree() : Promise.resolve([]),
- ]);
- const contactPhone = businessInfo?.phoneNumbers[0];
- const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
- const isFiltered = Boolean(params.q || params.categoryId);
- const categoryLabel = params.categoryId ? findCategoryLabel(categoryTree, params.categoryId) : undefined;
-
- const pageHref = (p: number) => {
- const sp = new URLSearchParams();
- if (params.categoryId) sp.set("categoryId", params.categoryId);
- if (params.q) sp.set("q", params.q);
- sp.set("page", String(p));
- return `/products?${sp.toString()}`;
- };
-
- return (
- {group?.title ?? "محصولات"}
- {items.length === 0 ? ( -
- {items.map((product) => (
-
- ))}
-
- )}
-
-
-
-
- ) : (
- <>
-
- );
+ const suffix = query.toString();
+ permanentRedirect(`/store-items${suffix ? `?${suffix}` : ""}`);
}
diff --git a/src/app/store-items/[id]/page.tsx b/src/app/store-items/[id]/page.tsx
index 9f6c587..08c7f97 100644
--- a/src/app/store-items/[id]/page.tsx
+++ b/src/app/store-items/[id]/page.tsx
@@ -1,5 +1,5 @@
import { notFound } from "next/navigation";
-import { Breadcrumb } from "@/components/ui/Breadcrumb";
+import { PageShell } from "@/components/ui/PageShell";
import { ProductGallery } from "@/components/product/ProductGallery";
import { ProductPurchasePanel } from "@/components/product/ProductPurchasePanel";
import { ProductTabs } from "@/components/product/ProductTabs";
@@ -7,6 +7,7 @@ import { SimilarProducts } from "@/components/product/SimilarProducts";
import {
getBusinessInfo,
getComments,
+ getProduct,
getProductById,
getProductStoreVariants,
getProductTechnicalInfo,
@@ -21,9 +22,9 @@ export default async function StoreItemDetailPage({
}) {
const { id } = await params;
const variant = await getStoreItemVariant(id);
- if (!variant) notFound();
-
- const product = await getProductById(variant.productId);
+ const product = variant
+ ? await getProductById(variant.productId)
+ : (await getProduct(id)) ?? (await getProductById(id));
if (!product) notFound();
const [technicalInfo, variations, businessInfo, comments] = await Promise.all([
@@ -39,21 +40,20 @@ export default async function StoreItemDetailPage({
// Land on the exact variant the shopper opened, not the first option of
// every variation.
const initialSelections = Object.fromEntries(
- variant.selections.map((s) => [s.variationId, s.optionId]),
+ (variant?.selections ?? []).map((s) => [s.variationId, s.optionId]),
);
return (
- - {params.q ? `نتایج جستجو برای «${params.q}»` : "محصولات"} -
- - {items.length === 0 ? ( -
- {items.map((product) => (
-
- ))}
-
-
- {totalPages > 1 && (
-
- )}
- >
- )}
-
-
+ ]}
+ >
();
+ if (categoryId) {
+ selectedCategoryIds.add(categoryId);
+ const parent = categories.find((item) => item.id === categoryId);
+ parent?.children.forEach((child) => selectedCategoryIds.add(child.id));
+ }
+
+ const specialItems = (specialGroup?.items ?? []).filter((product) => {
+ const matchesCategory = !categoryId || (product.categoryId && selectedCategoryIds.has(product.categoryId));
+ const matchesQuery = !params.q || product.name.toLocaleLowerCase("fa").includes(params.q.toLocaleLowerCase("fa"));
+ const matchesMin = minPrice == null || (product.price != null && product.price >= minPrice);
+ const matchesMax = maxPrice == null || (product.price != null && product.price <= maxPrice);
+ const matchesStock = !inStock || (product.stockQuantity != null && product.stockQuantity > 0);
+ return matchesCategory && matchesQuery && matchesMin && matchesMax && matchesStock;
+ });
+
+ const storeResult = specialMode
+ ? { items: [], total: 0 }
+ : await getStoreItems({
+ categoryId,
+ name: params.q,
+ minPrice,
+ maxPrice,
+ inStock: inStock || undefined,
+ page,
+ pageSize: PAGE_SIZE,
+ });
+ const totalPages = Math.max(1, Math.ceil(storeResult.total / PAGE_SIZE));
const pageHref = (p: number) => {
const sp = new URLSearchParams();
- if (params.categoryId) sp.set("categoryId", params.categoryId);
+ if (categoryId) sp.set("categoryId", categoryId);
if (params.q) sp.set("q", params.q);
+ if (minPrice != null) sp.set("minPrice", String(minPrice));
+ if (maxPrice != null) sp.set("maxPrice", String(maxPrice));
+ if (inStock) sp.set("inStock", "true");
sp.set("page", String(p));
return `/store-items?${sp.toString()}`;
};
@@ -41,16 +118,49 @@ export default async function StoreItemsPage({
]}
/>
-
- ) : (
- <>
-
@@ -101,6 +101,6 @@ export default async function StoreItemDetailPage({
excludeProductId={product.id}
contactPhone={contactPhone}
/>
-
+
);
}
diff --git a/src/app/store-items/page.tsx b/src/app/store-items/page.tsx
index 3475e6f..2efcb1a 100644
--- a/src/app/store-items/page.tsx
+++ b/src/app/store-items/page.tsx
@@ -1,33 +1,110 @@
import { Breadcrumb } from "@/components/ui/Breadcrumb";
import { ProductCard } from "@/components/ui/ProductCard";
import { EmptyState } from "@/components/ui/EmptyState";
-import { getBusinessInfo, getStoreItems, storeItemHref } from "@/lib/meshkee";
+import {
+ ProductFilters,
+ type ProductFilterCategory,
+} from "@/components/product/ProductFilters";
+import {
+ getCategoryTree,
+ getStoreItems,
+ getStoreSpecials,
+ isSpecialSaleGroup,
+ productHref,
+ storeItemHref,
+} from "@/lib/meshkee";
const PAGE_SIZE = 24;
export default async function StoreItemsPage({
searchParams,
}: {
- searchParams: Promise<{ categoryId?: string; q?: string; page?: string }>;
+ searchParams: Promise<{
+ categoryId?: string;
+ q?: string;
+ page?: string;
+ minPrice?: string;
+ maxPrice?: string;
+ inStock?: string;
+ specialId?: string;
+ special?: string;
+ }>;
}) {
const params = await searchParams;
const page = Math.max(1, Number(params.page ?? 1) || 1);
- const [{ items, total }, businessInfo] = await Promise.all([
- getStoreItems({
- categoryId: params.categoryId,
- name: params.q,
- page,
- pageSize: PAGE_SIZE,
- }),
- getBusinessInfo(),
+ const parsePrice = (value?: string) => {
+ const number = Number(value);
+ return value && Number.isFinite(number) && number >= 0 ? number : undefined;
+ };
+ const minPrice = parsePrice(params.minPrice);
+ const maxPrice = parsePrice(params.maxPrice);
+ const inStock = params.inStock === "true";
+ const categoryId = params.categoryId || undefined;
+
+ const specialMode = Boolean(params.specialId || params.special === "sale");
+ const [categoryTree, specialGroups] = await Promise.all([
+ getCategoryTree(),
+ specialMode ? getStoreSpecials() : Promise.resolve([]),
]);
- const contactPhone = businessInfo?.phoneNumbers[0];
- const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
+ const categories: ProductFilterCategory[] = categoryTree.flatMap((group) =>
+ group.items.map((category) => ({
+ id: category.id,
+ label: category.nameFa,
+ children: category.children.map((child) => ({
+ id: child.id,
+ label: child.nameFa,
+ })),
+ })),
+ );
+ const filterValues = {
+ categoryId,
+ minPrice: minPrice != null ? String(minPrice) : undefined,
+ maxPrice: maxPrice != null ? String(maxPrice) : undefined,
+ inStock,
+ q: params.q,
+ specialId: params.specialId,
+ special: params.special,
+ };
+
+ const specialGroup = params.specialId
+ ? specialGroups.find((group) => group.id === params.specialId)
+ : specialGroups.find(isSpecialSaleGroup);
+ const selectedCategoryIds = new Set- {params.q ? `نتایج جستجو برای «${params.q}»` : "فروشگاه"} -
+ {params.q && ( ++ نتایج جستجو برای «{params.q}» +
+ )} - {items.length === 0 ? ( -
- {items.map((item) => (
+
{node.nameFa}
@@ -158,7 +158,7 @@ function MobileDrawer({
{node.children.map((child) => (
{child.nameFa}
@@ -307,7 +307,7 @@ export function Header({
مشاهده همه
@@ -322,7 +322,7 @@ export function Header({
{level2.map((node) => (
+
+
+
);
}
diff --git a/src/components/blog/SimilarArticles.tsx b/src/components/blog/SimilarArticles.tsx
new file mode 100644
index 0000000..073876f
--- /dev/null
+++ b/src/components/blog/SimilarArticles.tsx
@@ -0,0 +1,67 @@
+import Link from "next/link";
+import { Placeholder } from "@/components/ui/Placeholder";
+import type { BlogSummary } from "@/lib/meshkee";
+
+function formatDate(iso?: string) {
+ if (!iso) return "";
+ return new Intl.DateTimeFormat("fa-IR", { dateStyle: "medium" }).format(new Date(iso));
+}
+
+export function SimilarArticles({ posts }: { posts: BlogSummary[] }) {
+ if (posts.length === 0) return null;
+
+ return (
+
+ );
+}
diff --git a/src/components/home/Header.tsx b/src/components/home/Header.tsx
index 6594d3d..9e650c0 100644
--- a/src/components/home/Header.tsx
+++ b/src/components/home/Header.tsx
@@ -10,7 +10,7 @@ import { SearchBar } from "./SearchBar";
const navLinks = [
{ label: "فروشگاه", href: "/store-items" },
- { label: "فروش ویژه", href: "#" },
+ { label: "فروش ویژه", href: "/store-items?special=sale" },
{ label: "درباره ما", href: "#" },
{ label: "سوالات متداول", href: "#" },
{ label: "تماس با ما", href: "/contact" },
@@ -148,7 +148,7 @@ function MobileDrawer({
{level2.map((node) => (
+ {specialMode && (
+
+ ) : specialMode ? (
+
+ {specialGroup?.title ?? "فروش ویژه"} +
+ )} + + {(specialMode ? specialItems : storeResult.items).length === 0 ? ( +
+ {specialItems.map((product) => (
+
+ ))}
+
+ ) : (
+ <>
+
+ {storeResult.items.map((item) => (
))}
- {totalPages > 1 && (
+ {!specialMode && totalPages > 1 && (
{page > 1 ? (
)}
- >
- )}
+ >
+ )}
+
+
{node.nameFa}
@@ -332,7 +332,7 @@ export function Header({
{node.children.map((child) => (
{child.nameFa}
@@ -351,7 +351,7 @@ export function Header({
{categories.slice(0, 4).map((cat) => (
{cat.name}
diff --git a/src/components/home/SearchBar.tsx b/src/components/home/SearchBar.tsx
index 34c7b1f..c7229b8 100644
--- a/src/components/home/SearchBar.tsx
+++ b/src/components/home/SearchBar.tsx
@@ -1,11 +1,13 @@
"use client";
import { useEffect, useRef, useState } from "react";
+import { useRouter } from "next/navigation";
import { Placeholder } from "@/components/ui/Placeholder";
import { formatPrice } from "@/data/sample";
import type { Product } from "@/lib/meshkee";
export function SearchBar() {
+ const router = useRouter();
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [open, setOpen] = useState(false);
@@ -15,8 +17,6 @@ export function SearchBar() {
useEffect(() => {
const trimmed = query.trim();
if (trimmed.length <= 2) {
- setResults([]);
- setOpen(false);
return;
}
@@ -56,7 +56,7 @@ export function SearchBar() {
const submit = () => {
const trimmed = query.trim();
- if (trimmed) window.location.href = `/products?q=${encodeURIComponent(trimmed)}`;
+ if (trimmed) router.push(`/store-items?q=${encodeURIComponent(trimmed)}`);
};
return (
@@ -64,7 +64,14 @@ export function SearchBar() {
setQuery(event.target.value)}
+ onChange={(event) => {
+ const value = event.target.value;
+ setQuery(value);
+ if (value.trim().length <= 2) {
+ setResults([]);
+ setOpen(false);
+ }
+ }}
onFocus={() => results.length > 0 && setOpen(true)}
onKeyDown={(event) => event.key === "Enter" && submit()}
placeholder="جستجوی محصول، برند یا دستهبندی..."
@@ -89,7 +96,7 @@ export function SearchBar() {
{results.map((product) => (
@@ -48,7 +49,7 @@ function SpecialSaleSection({ group }: { group: ProductGroup }) {
مشاهده همه
@@ -127,7 +128,7 @@ export function StoreSpecialCarousels({
title={group.title}
enTitle={englishLabelFromKey(group.key)}
action="مشاهده همه"
- actionHref={`/products?specialId=${group.id}`}
+ actionHref={`/store-items?specialId=${group.id}`}
/>
{group.items.map((product) => (
diff --git a/src/components/product/ProductFilters.tsx b/src/components/product/ProductFilters.tsx
new file mode 100644
index 0000000..ef4eda9
--- /dev/null
+++ b/src/components/product/ProductFilters.tsx
@@ -0,0 +1,360 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+import Form from "next/form";
+import { CloseIcon } from "@/components/ui/icons";
+
+export type ProductFilterCategory = {
+ id: string;
+ label: string;
+ children: { id: string; label: string }[];
+};
+
+export type ProductFilterValues = {
+ categoryId?: string;
+ minPrice?: string;
+ maxPrice?: string;
+ inStock: boolean;
+ q?: string;
+ specialId?: string;
+ special?: string;
+};
+
+function FilterIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
+function FilterFields({
+ categories,
+ values,
+}: {
+ categories: ProductFilterCategory[];
+ values: ProductFilterValues;
+}) {
+ const categoryListRef = useRef(null);
+
+ useEffect(() => {
+ if (!values.categoryId) return;
+ const activeParent = categories.find(
+ (category) =>
+ category.id === values.categoryId ||
+ (category.children ?? []).some((child) => child.id === values.categoryId),
+ );
+ if (!activeParent) return;
+ const accordion = categoryListRef.current?.querySelector(
+ `[data-category-id="${CSS.escape(activeParent.id)}"]`,
+ );
+ if (accordion) accordion.open = true;
+ }, [categories, values.categoryId]);
+
+ return (
+ | null>(null);
+ const activeCount = [values.categoryId, values.minPrice || values.maxPrice, values.inStock].filter(
+ Boolean,
+ ).length;
+ const resetParams = new URLSearchParams();
+ if (values.q) resetParams.set("q", values.q);
+ if (values.specialId) resetParams.set("specialId", values.specialId);
+ if (values.special) resetParams.set("special", values.special);
+ const resetQuery = resetParams.toString();
+ const resetHref = `/store-items${resetQuery ? `?${resetQuery}` : ""}`;
+
+ useEffect(() => {
+ if (!open) return;
+ const previousOverflow = document.body.style.overflow;
+ const onKeyDown = (event: KeyboardEvent) => {
+ if (event.key === "Escape") setOpen(false);
+ };
+ document.body.style.overflow = "hidden";
+ document.addEventListener("keydown", onKeyDown);
+ return () => {
+ document.body.style.overflow = previousOverflow;
+ document.removeEventListener("keydown", onKeyDown);
+ };
+ }, [open]);
+
+ useEffect(
+ () => () => {
+ if (desktopPriceTimer.current) clearTimeout(desktopPriceTimer.current);
+ },
+ [],
+ );
+
+ const submitDesktopFilters = (event: React.ChangeEvent) => {
+ const form = event.currentTarget;
+ const target = event.target;
+ if (!(target instanceof HTMLInputElement)) return;
+
+ if (desktopPriceTimer.current) clearTimeout(desktopPriceTimer.current);
+ if (target.type === "number") {
+ desktopPriceTimer.current = setTimeout(() => form.requestSubmit(), 600);
+ return;
+ }
+ form.requestSubmit();
+ };
+
+ return (
+ <>
+
+
+
+
+ {open && (
+
تا پایان فروش ویژه
+ {values.q && }
+ {values.specialId && }
+ {values.special && }
+
+
+ );
+}
+
+function FilterActions({ resetHref }: { resetHref: string }) {
+ return (
+
+
+
+ + دستهبندی + + +
+
+
+ {categories.map((category) => (
+ (category.children ?? []).length > 0 ? (
+
+
+
+ ) : (
+
+ )
+ ))}
+ + + +
+
+ {(category.children ?? []).map((child) => (
+
+ ))}
+
+
+
+
+ + محدوده قیمت + + +
+
+
+
+
+ مبالغ به تومان هستند.
+
+
+
+
+
+
+ پاک کردن
+
+
+ );
+}
+
+export function ProductFilters({
+ categories,
+ values,
+}: {
+ categories: ProductFilterCategory[];
+ values: ProductFilterValues;
+}) {
+ const [open, setOpen] = useState(false);
+ const desktopPriceTimer = useRef {
+ if (event.target === event.currentTarget) setOpen(false);
+ }}
+ >
+
+ )}
+ >
+ );
+}
diff --git a/src/components/product/ProductGallery.tsx b/src/components/product/ProductGallery.tsx
index 79a46cf..7dbdb8a 100644
--- a/src/components/product/ProductGallery.tsx
+++ b/src/components/product/ProductGallery.tsx
@@ -33,11 +33,8 @@ function Lightbox({
onIndexChange: (i: number) => void;
onClose: () => void;
}) {
- const [mounted, setMounted] = useState(false);
const hasMultiple = images.length > 1;
- useEffect(() => setMounted(true), []);
-
useEffect(() => {
document.body.style.overflow = "hidden";
const onKeyDown = (event: KeyboardEvent) => {
@@ -52,8 +49,6 @@ function Lightbox({
};
}, [index, images.length, onClose, onIndexChange]);
- if (!mounted) return null;
-
return createPortal(
+
+
+
+
+
+ + فیلتر محصولات +
+ +
>
)}
@@ -164,7 +159,7 @@ export function ProductGallery({ images, alt }: { images: string[]; alt: string
aria-label="تصویر بعدی"
className="absolute top-1/2 right-3 z-10 flex h-9 w-9 -translate-y-1/2 items-center justify-center rounded-full border border-border bg-surface/90 text-foreground shadow-sm transition-colors hover:border-red hover:text-red"
>
-
+
>
)}
diff --git a/src/components/product/ProductTabs.tsx b/src/components/product/ProductTabs.tsx
index 92abf2f..f928459 100644
--- a/src/components/product/ProductTabs.tsx
+++ b/src/components/product/ProductTabs.tsx
@@ -119,8 +119,8 @@ export function ProductTabs({
const activeLabel = TABS.find((t) => t.id === active)?.label ?? "";
return (
-
-
+
+
{TABS.map((tab) => (