view products

This commit is contained in:
hamid zarghami
2026-02-14 12:04:59 +03:30
parent a8deff9267
commit fd8a749aed
5 changed files with 167 additions and 12 deletions
+51 -3
View File
@@ -2,9 +2,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import SearchBox from "@/components/input/SearchBox";
import TextAlignIcon from "@/components/icons/TextAlignIcon";
import ListIcon from "@/components/icons/ListIcon";
import GridIcon from "@/components/icons/GridIcon";
import VerticalScrollView from "@/components/listview/VerticalScrollView";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import MenuItem from "@/components/listview/MenuItem";
import MenuItem, { type MenuItemViewMode } from "@/components/listview/MenuItem";
import { useQueryState } from "next-usequerystate";
import clsx from "clsx";
import { motion } from "framer-motion";
@@ -15,6 +17,7 @@ import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer";
import MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton";
import { useGetCategories, useGetProducts } from "./hooks/useMenuData";
import type { Product, Category } from "./types/Types";
import { RowHorizontal, RowVertical } from "iconsax-react";
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
@@ -36,6 +39,17 @@ const MenuIndex = () => {
const wrapperRef: React.RefObject<HTMLDivElement | null> = useRef(null);
const [smallCategoriesVisible, setSmallCategoriesVisibility] = useState(false);
const [isInitialMount, setIsInitialMount] = useState(true);
const [viewMode, setViewMode] = useState<MenuItemViewMode>("list");
useEffect(() => {
const stored = localStorage.getItem("menuViewMode");
if (stored === "grid" || stored === "list") setViewMode(stored);
}, []);
const setViewModeAndPersist = useCallback((mode: MenuItemViewMode) => {
setViewMode(mode);
if (typeof window !== "undefined") localStorage.setItem("menuViewMode", mode);
}, []);
useEffect(() => {
if (isInitialMount) {
@@ -160,6 +174,32 @@ const MenuIndex = () => {
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
</span>
<div className="inline-flex gap-2 justify-end items-center">
<div className="inline-flex rounded-xl h-8 bg-container p-1">
<button
onClick={() => setViewModeAndPersist("list")}
className={clsx(
"rounded-lg h-6 w-8 inline-flex items-center justify-center transition-colors",
viewMode === "list"
? "bg-background text-foreground"
: "text-foreground/60 hover:text-foreground"
)}
title="نمایش یک ستونه"
>
<RowVertical color="black" className="w-4 h-4" />
</button>
<button
onClick={() => setViewModeAndPersist("grid")}
className={clsx(
"rounded-lg h-6 w-8 inline-flex items-center justify-center transition-colors",
viewMode === "grid"
? "bg-background text-foreground"
: "text-foreground/60 hover:text-foreground"
)}
title="نمایش دو ستونه"
>
<RowHorizontal color="black" className="w-4 h-4" />
</button>
</div>
<button
onClick={toggleFilterSortModal}
className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center gap-2"
@@ -191,10 +231,18 @@ const MenuIndex = () => {
<div className="text-center text-foreground/60 py-8">
{productsData ? 'کالایی یافت نشد' : 'در حال بارگذاری...'}
</div>
) : viewMode === "grid" ? (
<div className="grid grid-cols-2 gap-3">
{filteredReceiptItems.map((product) => (
<MenuItemRenderer key={product.id} variant="grid">
<MenuItem product={product} viewMode="grid" />
</MenuItemRenderer>
))}
</div>
) : (
filteredReceiptItems.map((product) => (
<MenuItemRenderer key={product.id}>
<MenuItem product={product} />
<MenuItemRenderer key={product.id} variant="list">
<MenuItem product={product} viewMode="list" />
</MenuItemRenderer>
))
)}
+4 -1
View File
@@ -16,6 +16,8 @@ export interface CartQuantityControlProps {
addLabel?: string
/** متن دکمه در حالت loading */
addingLabel?: string
/** عرض کامل (۱۰۰٪) */
fullWidth?: boolean
}
const Spinner = () => (
@@ -30,11 +32,12 @@ export function CartQuantityControl({
addDisabled = false,
addLabel = 'افزودن',
addingLabel = 'در حال افزودن...',
fullWidth = false,
}: CartQuantityControlProps) {
return (
<motion.div
whileTap={{ scale: 1.05 }}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden"
className={`bg-background active:drop-shadow-xs rounded-md h-8 inline-flex p-1 justify-between items-center gap-2 relative overflow-hidden ${fullWidth ? 'w-full' : 'max-w-[115px] w-full'}`}
>
{quantity <= 0 ? (
<button
+36
View File
@@ -0,0 +1,36 @@
import React from 'react';
const ListIcon = (props: React.SVGProps<SVGSVGElement>) => (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M2 4H14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 8H14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 12H14"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default ListIcon;
+62 -4
View File
@@ -13,13 +13,17 @@ import {
getProductEffectivePrice,
} from "@/app/[name]/(Main)/types/Types";
export type MenuItemViewMode = 'list' | 'grid';
interface MenuItemProps {
product: Product;
/** در صفحه سبد هر خط با variantId مشخص می‌شود */
variantId?: string | null;
/** حالت نمایش: list = یک ستونه (پیش‌فرض)، grid = دو ستونه */
viewMode?: MenuItemViewMode;
}
const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
const MenuItem = ({ product, variantId: variantIdProp, viewMode = 'list' }: MenuItemProps) => {
const { items, addToCart, removeFromCart, isCartMutating } = useCart();
const params = useParams<{ name: string }>();
const name = params?.name || "";
@@ -112,6 +116,60 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
if (viewMode === 'grid') {
return (
<div className="flex flex-col w-full h-full">
<Link href={productDetailUrl} className="cursor-pointer rounded-xl aspect-square overflow-hidden flex items-center justify-center max-h-[100px]">
<img
className="rounded-xl max-w-full max-h-full object-center"
src={resolvedImage}
alt={productName}
/>
</Link>
<Link href={productDetailUrl} className="cursor-pointer min-w-0 mt-4">
<div className="text-sm2 font-normal text-black dark:text-white wrap-break-word line-clamp-2">
{productName}
</div>
</Link>
<div className="flex flex-col gap-2 mt-1">
<div className="flex flex-col gap-1" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">
{formattedOriginalPrice} T
</span>
<span className="text-sm font-medium text-primary dark:text-foreground">
{formattedPrice} T
</span>
</>
) : (
<span className="text-sm text-right">
{formattedPrice} T
</span>
)}
</div>
{showDetailsInsteadOfAdd ? (
<Link
href={productDetailUrl}
className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
>
جزئیات
</Link>
) : (
<CartQuantityControl
quantity={quantity}
onAdd={handleAddToCart}
onRemove={handleRemoveFromCart}
isMutating={isCartMutating}
fullWidth
/>
)}
</div>
</div>
);
}
return (
<div className="flex gap-4 w-full h-full items-center">
<Link href={productDetailUrl} className="cursor-pointer">
@@ -136,8 +194,8 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
)}
</div>
</Link>
<div className="inline-flex mt-2 gap-2 justify-between w-full items-center">
<div className="w-full flex flex-col gap-1" dir="ltr">
<div className="flex flex-col mt-2 gap-2 w-full">
<div className="flex flex-col gap-1" dir="ltr">
{hasDiscount ? (
<>
<span className="text-xs text-disabled-text line-through">
@@ -156,7 +214,7 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
{showDetailsInsteadOfAdd ? (
<Link
href={productDetailUrl}
className="bg-background active:drop-shadow-xs max-w-[115px] rounded-md w-full h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
className="bg-background active:drop-shadow-xs w-full rounded-md h-8 inline-flex justify-center items-center gap-2 px-2 text-sm2 font-normal text-foreground"
>
جزئیات
</Link>
+14 -4
View File
@@ -4,14 +4,20 @@ import React from 'react';
type Props = {
children: React.ReactNode;
variant?: 'list' | 'grid';
} & React.HTMLAttributes<HTMLDivElement>;
function MenuItemRendererComponent({ children, ...rest }: Props) {
function MenuItemRendererComponent({ children, variant = 'list', ...rest }: Props) {
const isGrid = variant === 'grid';
return (
<div
{...rest}
className={`${rest.className} box-shadow-normal !pb-4 py-4 transition-all duration-200 ease-out w-full bg-container rounded-3xl px-4 flex items-center justify-between gap-4`}>
className={`${rest.className} box-shadow-normal transition-all duration-200 ease-out w-full bg-container rounded-3xl ${
isGrid
? 'p-3 flex flex-col'
: 'py-4 pb-4! px-4 flex items-center justify-between gap-4'
}`}
>
{children}
</div>
);
@@ -19,7 +25,11 @@ function MenuItemRendererComponent({ children, ...rest }: Props) {
// Memoize with shallow comparison of props
const MenuItemRenderer = React.memo(MenuItemRendererComponent, (prevProps, nextProps) => {
return prevProps.className === nextProps.className && prevProps.children === nextProps.children;
return (
prevProps.className === nextProps.className &&
prevProps.children === nextProps.children &&
prevProps.variant === nextProps.variant
);
});
export default MenuItemRenderer;