view products
This commit is contained in:
@@ -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>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user