view products
This commit is contained in:
@@ -2,9 +2,11 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import SearchBox from "@/components/input/SearchBox";
|
import SearchBox from "@/components/input/SearchBox";
|
||||||
import TextAlignIcon from "@/components/icons/TextAlignIcon";
|
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 VerticalScrollView from "@/components/listview/VerticalScrollView";
|
||||||
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
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 { useQueryState } from "next-usequerystate";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { motion } from "framer-motion";
|
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 MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton";
|
||||||
import { useGetCategories, useGetProducts } from "./hooks/useMenuData";
|
import { useGetCategories, useGetProducts } from "./hooks/useMenuData";
|
||||||
import type { Product, Category } from "./types/Types";
|
import type { Product, Category } from "./types/Types";
|
||||||
|
import { RowHorizontal, RowVertical } from "iconsax-react";
|
||||||
|
|
||||||
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
||||||
|
|
||||||
@@ -36,6 +39,17 @@ const MenuIndex = () => {
|
|||||||
const wrapperRef: React.RefObject<HTMLDivElement | null> = useRef(null);
|
const wrapperRef: React.RefObject<HTMLDivElement | null> = useRef(null);
|
||||||
const [smallCategoriesVisible, setSmallCategoriesVisibility] = useState(false);
|
const [smallCategoriesVisible, setSmallCategoriesVisibility] = useState(false);
|
||||||
const [isInitialMount, setIsInitialMount] = useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isInitialMount) {
|
if (isInitialMount) {
|
||||||
@@ -160,6 +174,32 @@ const MenuIndex = () => {
|
|||||||
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
|
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="inline-flex gap-2 justify-end items-center">
|
<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
|
<button
|
||||||
onClick={toggleFilterSortModal}
|
onClick={toggleFilterSortModal}
|
||||||
className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center gap-2"
|
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">
|
<div className="text-center text-foreground/60 py-8">
|
||||||
{productsData ? 'کالایی یافت نشد' : 'در حال بارگذاری...'}
|
{productsData ? 'کالایی یافت نشد' : 'در حال بارگذاری...'}
|
||||||
</div>
|
</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) => (
|
filteredReceiptItems.map((product) => (
|
||||||
<MenuItemRenderer key={product.id}>
|
<MenuItemRenderer key={product.id} variant="list">
|
||||||
<MenuItem product={product} />
|
<MenuItem product={product} viewMode="list" />
|
||||||
</MenuItemRenderer>
|
</MenuItemRenderer>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface CartQuantityControlProps {
|
|||||||
addLabel?: string
|
addLabel?: string
|
||||||
/** متن دکمه در حالت loading */
|
/** متن دکمه در حالت loading */
|
||||||
addingLabel?: string
|
addingLabel?: string
|
||||||
|
/** عرض کامل (۱۰۰٪) */
|
||||||
|
fullWidth?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Spinner = () => (
|
const Spinner = () => (
|
||||||
@@ -30,11 +32,12 @@ export function CartQuantityControl({
|
|||||||
addDisabled = false,
|
addDisabled = false,
|
||||||
addLabel = 'افزودن',
|
addLabel = 'افزودن',
|
||||||
addingLabel = 'در حال افزودن...',
|
addingLabel = 'در حال افزودن...',
|
||||||
|
fullWidth = false,
|
||||||
}: CartQuantityControlProps) {
|
}: CartQuantityControlProps) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
whileTap={{ scale: 1.05 }}
|
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 ? (
|
{quantity <= 0 ? (
|
||||||
<button
|
<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,
|
getProductEffectivePrice,
|
||||||
} from "@/app/[name]/(Main)/types/Types";
|
} from "@/app/[name]/(Main)/types/Types";
|
||||||
|
|
||||||
|
export type MenuItemViewMode = 'list' | 'grid';
|
||||||
|
|
||||||
interface MenuItemProps {
|
interface MenuItemProps {
|
||||||
product: Product;
|
product: Product;
|
||||||
/** در صفحه سبد هر خط با variantId مشخص میشود */
|
/** در صفحه سبد هر خط با variantId مشخص میشود */
|
||||||
variantId?: string | null;
|
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 { items, addToCart, removeFromCart, isCartMutating } = useCart();
|
||||||
const params = useParams<{ name: string }>();
|
const params = useParams<{ name: string }>();
|
||||||
const name = params?.name || "";
|
const name = params?.name || "";
|
||||||
@@ -112,6 +116,60 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
|
|||||||
|
|
||||||
const productDetailUrl = `/${name}/${product.id}?category=${product.category.id}`;
|
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 (
|
return (
|
||||||
<div className="flex gap-4 w-full h-full items-center">
|
<div className="flex gap-4 w-full h-full items-center">
|
||||||
<Link href={productDetailUrl} className="cursor-pointer">
|
<Link href={productDetailUrl} className="cursor-pointer">
|
||||||
@@ -136,8 +194,8 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="inline-flex mt-2 gap-2 justify-between w-full items-center">
|
<div className="flex flex-col mt-2 gap-2 w-full">
|
||||||
<div className="w-full flex flex-col gap-1" dir="ltr">
|
<div className="flex flex-col gap-1" dir="ltr">
|
||||||
{hasDiscount ? (
|
{hasDiscount ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-xs text-disabled-text line-through">
|
<span className="text-xs text-disabled-text line-through">
|
||||||
@@ -156,7 +214,7 @@ const MenuItem = ({ product, variantId: variantIdProp }: MenuItemProps) => {
|
|||||||
{showDetailsInsteadOfAdd ? (
|
{showDetailsInsteadOfAdd ? (
|
||||||
<Link
|
<Link
|
||||||
href={productDetailUrl}
|
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>
|
</Link>
|
||||||
|
|||||||
@@ -4,14 +4,20 @@ import React from 'react';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
variant?: 'list' | 'grid';
|
||||||
} & React.HTMLAttributes<HTMLDivElement>;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
function MenuItemRendererComponent({ children, ...rest }: Props) {
|
function MenuItemRendererComponent({ children, variant = 'list', ...rest }: Props) {
|
||||||
|
const isGrid = variant === 'grid';
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...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}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -19,7 +25,11 @@ function MenuItemRendererComponent({ children, ...rest }: Props) {
|
|||||||
|
|
||||||
// Memoize with shallow comparison of props
|
// Memoize with shallow comparison of props
|
||||||
const MenuItemRenderer = React.memo(MenuItemRendererComponent, (prevProps, nextProps) => {
|
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;
|
export default MenuItemRenderer;
|
||||||
|
|||||||
Reference in New Issue
Block a user