merge filter and sort
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import Button from "@/components/button/PrimaryButton";
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { TicketPercentIcon } from "lucide-react";
|
import { TicketPercentIcon, ChevronDown } from "lucide-react";
|
||||||
|
import clsx from "clsx";
|
||||||
|
|
||||||
type MenuFilterDrawerProps = {
|
type MenuFilterDrawerProps = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -12,6 +13,9 @@ type MenuFilterDrawerProps = {
|
|||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
onSearchChange: (value: string) => void;
|
onSearchChange: (value: string) => void;
|
||||||
onApply: () => void;
|
onApply: () => void;
|
||||||
|
sortings: ReadonlyArray<string>;
|
||||||
|
activeSortIndex: number;
|
||||||
|
onSortChange: (index: number) => void;
|
||||||
tMenu: (key: string) => string;
|
tMenu: (key: string) => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -21,15 +25,34 @@ const MenuFilterDrawer = ({
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
onSearchChange,
|
onSearchChange,
|
||||||
onApply,
|
onApply,
|
||||||
|
sortings,
|
||||||
|
activeSortIndex,
|
||||||
|
onSortChange,
|
||||||
tMenu,
|
tMenu,
|
||||||
}: MenuFilterDrawerProps) => {
|
}: MenuFilterDrawerProps) => {
|
||||||
const [tempQuery, setTempQuery] = useState(searchQuery);
|
const [tempQuery, setTempQuery] = useState(searchQuery);
|
||||||
|
const [tempSortIndex, setTempSortIndex] = useState(activeSortIndex);
|
||||||
|
const [sortSelectOpen, setSortSelectOpen] = useState(false);
|
||||||
|
const sortSelectRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setTempQuery(searchQuery);
|
setTempQuery(searchQuery);
|
||||||
|
setTempSortIndex(activeSortIndex);
|
||||||
|
setSortSelectOpen(false);
|
||||||
}
|
}
|
||||||
}, [visible, searchQuery]);
|
}, [visible, searchQuery, activeSortIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sortSelectOpen || !visible) return;
|
||||||
|
const handleClickOutside = (e: MouseEvent) => {
|
||||||
|
if (sortSelectRef.current && !sortSelectRef.current.contains(e.target as Node)) {
|
||||||
|
setSortSelectOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, [sortSelectOpen, visible]);
|
||||||
|
|
||||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setTempQuery(e.target.value);
|
setTempQuery(e.target.value);
|
||||||
@@ -37,12 +60,14 @@ const MenuFilterDrawer = ({
|
|||||||
|
|
||||||
const handleApply = () => {
|
const handleApply = () => {
|
||||||
onSearchChange(tempQuery);
|
onSearchChange(tempQuery);
|
||||||
|
onSortChange(tempSortIndex);
|
||||||
onApply();
|
onApply();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setTempQuery(searchQuery);
|
setTempQuery(searchQuery);
|
||||||
|
setTempSortIndex(activeSortIndex);
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +79,7 @@ const MenuFilterDrawer = ({
|
|||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div className="px-6 mt-5">
|
<div className="px-6 mt-5">
|
||||||
|
{/* فیلتر: جستجو */}
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label className="text-sm2 text-foreground">
|
<label className="text-sm2 text-foreground">
|
||||||
{tMenu("MenuFilterDrawer.SelectContent.Label")}
|
{tMenu("MenuFilterDrawer.SelectContent.Label")}
|
||||||
@@ -66,6 +92,7 @@ const MenuFilterDrawer = ({
|
|||||||
className="w-full h-11 px-3 py-2.5 rounded-normal border border-border bg-container/29 focus:outline-none focus:ring-2 focus:ring-black text-xs!"
|
className="w-full h-11 px-3 py-2.5 rounded-normal border border-border bg-container/29 focus:outline-none focus:ring-2 focus:ring-black text-xs!"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{/* فیلتر: دارای کد تخفیف */}
|
||||||
<div className="flex w-full mt-[23px] h-11 items-center justify-between gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-border cursor-pointer focus:outline-none focus:ring-2 focus:ring-black">
|
<div className="flex w-full mt-[23px] h-11 items-center justify-between gap-2 px-3 py-4 relative bg-container/29 rounded-xl border border-solid border-border cursor-pointer focus:outline-none focus:ring-2 focus:ring-black">
|
||||||
<span className="inline-flex items-center gap-2.5 text-sm2">
|
<span className="inline-flex items-center gap-2.5 text-sm2">
|
||||||
<TicketPercentIcon size={16} />
|
<TicketPercentIcon size={16} />
|
||||||
@@ -73,6 +100,46 @@ const MenuFilterDrawer = ({
|
|||||||
</span>
|
</span>
|
||||||
<Switch />
|
<Switch />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* مرتبسازی - Select با کلیک باز میشود */}
|
||||||
|
<div className="mt-6" ref={sortSelectRef}>
|
||||||
|
<label className="text-sm2 text-foreground block mb-3">
|
||||||
|
{tMenu("MenuSortingDrawer.Heading")}
|
||||||
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSortSelectOpen((prev) => !prev)}
|
||||||
|
className="w-full h-11 px-3 py-2.5 rounded-xl border border-border bg-container/29 flex items-center justify-between gap-2 text-right text-sm2 text-foreground focus:outline-none focus:ring-2 focus:ring-black"
|
||||||
|
>
|
||||||
|
<span>{tMenu(`MenuSortingDrawer.Options.${sortings[tempSortIndex]}`)}</span>
|
||||||
|
<ChevronDown
|
||||||
|
className={clsx("w-4 h-4 shrink-0 transition-transform", sortSelectOpen && "rotate-180")}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{sortSelectOpen && (
|
||||||
|
<div className="mt-1 rounded-xl border border-border bg-container/29 overflow-hidden">
|
||||||
|
{sortings.map((value, index) => {
|
||||||
|
const isActive = index === tempSortIndex;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setTempSortIndex(index);
|
||||||
|
setSortSelectOpen(false);
|
||||||
|
}}
|
||||||
|
className={clsx(
|
||||||
|
"flex items-center justify-between w-full py-3 px-3 text-sm2 transition-colors text-right",
|
||||||
|
isActive ? "text-primary font-semibold bg-primary/10" : "text-foreground hover:bg-muted/50"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>{tMenu(`MenuSortingDrawer.Options.${value}`)}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className="text-white/40 mt-12" />
|
<hr className="text-white/40 mt-12" />
|
||||||
<div className="px-9 pt-6 flex justify-between gap-[22px]">
|
<div className="px-9 pt-6 flex justify-between gap-[22px]">
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
type MenuSortingDrawerProps = {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
sortings: ReadonlyArray<string>;
|
|
||||||
activeIndex: number;
|
|
||||||
onSelect: (index: number) => void;
|
|
||||||
tMenu: (key: string) => string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const MenuSortingDrawer = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
sortings,
|
|
||||||
activeIndex,
|
|
||||||
onSelect,
|
|
||||||
tMenu,
|
|
||||||
}: MenuSortingDrawerProps) => {
|
|
||||||
return (
|
|
||||||
<AnimatedBottomSheet
|
|
||||||
title={tMenu("MenuSortingDrawer.Heading")}
|
|
||||||
visible={visible}
|
|
||||||
inDelay={150}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<div className="px-8.5 py-10 flex flex-col gap-2">
|
|
||||||
{sortings.map((value, index) => {
|
|
||||||
const isActive = index === activeIndex;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelect(index)}
|
|
||||||
className={clsx(
|
|
||||||
"flex items-center justify-between py-3 text-sm2 transition-colors",
|
|
||||||
isActive ? "text-primary font-semibold" : "text-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{tMenu(`MenuSortingDrawer.Options.${value}`)}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</AnimatedBottomSheet>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MenuSortingDrawer;
|
|
||||||
|
|
||||||
@@ -5,7 +5,6 @@ import TextAlignIcon from "@/components/icons/TextAlignIcon";
|
|||||||
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 from "@/components/listview/MenuItem";
|
||||||
import { Candle2 } from "iconsax-react";
|
|
||||||
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";
|
||||||
@@ -13,7 +12,6 @@ import { useTranslation } from "react-i18next";
|
|||||||
import useToggle from "@/hooks/helpers/useToggle";
|
import useToggle from "@/hooks/helpers/useToggle";
|
||||||
import CategoryScroll from "@/app/[name]/(Main)/components/CategoryScroll";
|
import CategoryScroll from "@/app/[name]/(Main)/components/CategoryScroll";
|
||||||
import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer";
|
import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer";
|
||||||
import MenuSortingDrawer from "@/app/[name]/(Main)/components/MenuSortingDrawer";
|
|
||||||
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";
|
||||||
@@ -29,8 +27,7 @@ const MenuIndex = () => {
|
|||||||
const isLoading = productsLoading || categoriesLoading;
|
const isLoading = productsLoading || categoriesLoading;
|
||||||
const { t: tCommon } = useTranslation('common');
|
const { t: tCommon } = useTranslation('common');
|
||||||
const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" });
|
const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" });
|
||||||
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
const { state: filterSortModal, toggle: toggleFilterSortModal } = useToggle();
|
||||||
const { state: sortingModal, toggle: toggleSortingModal } = useToggle();
|
|
||||||
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
|
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
|
||||||
const [search, setSearch] = useQueryState("q", { defaultValue: '' });
|
const [search, setSearch] = useQueryState("q", { defaultValue: '' });
|
||||||
const [selectedCategory, setSelectedCategory] = useQueryState('category', { defaultValue: '0' });
|
const [selectedCategory, setSelectedCategory] = useQueryState('category', { defaultValue: '0' });
|
||||||
@@ -85,13 +82,10 @@ const MenuIndex = () => {
|
|||||||
|
|
||||||
const sortingIndex = Number(sorting);
|
const sortingIndex = Number(sorting);
|
||||||
const activeSortingIndex = Number.isNaN(sortingIndex) ? 0 : sortingIndex;
|
const activeSortingIndex = Number.isNaN(sortingIndex) ? 0 : sortingIndex;
|
||||||
const activeSortingKey = sortings[activeSortingIndex] ?? sortings[0];
|
|
||||||
const activeSortingLabel = tMenu(`MenuSortingDrawer.Options.${activeSortingKey}`);
|
|
||||||
|
|
||||||
const changeSorting = (index: number) => {
|
const changeSorting = useCallback((index: number) => {
|
||||||
setSorting(() => String(index));
|
setSorting(() => String(index));
|
||||||
toggleSortingModal();
|
}, [setSorting]);
|
||||||
};
|
|
||||||
|
|
||||||
const changeFilterSearch = useCallback((value: string) => {
|
const changeFilterSearch = useCallback((value: string) => {
|
||||||
setFilterSearch(value);
|
setFilterSearch(value);
|
||||||
@@ -105,7 +99,7 @@ const MenuIndex = () => {
|
|||||||
|
|
||||||
const filtered = products.filter((item) => {
|
const filtered = products.filter((item) => {
|
||||||
const matchesCategory = !selectedCatId || item.category?.id === selectedCatId;
|
const matchesCategory = !selectedCatId || item.category?.id === selectedCatId;
|
||||||
const itemName = item.name || item.title || item.foodName || item.productName || "";
|
const itemName = item.name ?? item.title;
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
!search || itemName.toLowerCase().includes(lowerSearch);
|
!search || itemName.toLowerCase().includes(lowerSearch);
|
||||||
|
|
||||||
@@ -165,17 +159,13 @@ const MenuIndex = () => {
|
|||||||
<span className="sm:text-base text-sm font-medium">
|
<span className="sm:text-base text-sm font-medium">
|
||||||
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
|
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
|
||||||
</span>
|
</span>
|
||||||
<div className="inline-flex min-w-[247px] gap-2 justify-around items-center">
|
<div className="inline-flex gap-2 justify-end items-center">
|
||||||
<button onClick={toggleFilterModal} className="rounded-xl h-8 bg-container ps-4 pe-2 py-1.5 inline-flex items-center justify-between gap-[7px]">
|
|
||||||
<Candle2 className="stroke-foreground" size={16} />
|
|
||||||
<span className="text-xs leading-5 font-medium">{tMenu('MenuFilterDrawer.Label')}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={toggleSortingModal}
|
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"
|
||||||
>
|
>
|
||||||
<TextAlignIcon className="text-foreground" />
|
<TextAlignIcon className="text-foreground" />
|
||||||
<span className="text-xs leading-5 font-medium">{activeSortingLabel}</span>
|
<span className="text-xs leading-5 font-medium">{tMenu('MenuFilterDrawer.Label')}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,20 +202,14 @@ const MenuIndex = () => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<MenuFilterDrawer
|
<MenuFilterDrawer
|
||||||
visible={filterModal}
|
visible={filterSortModal}
|
||||||
onClose={toggleFilterModal}
|
onClose={toggleFilterSortModal}
|
||||||
searchQuery={filterSearch}
|
searchQuery={filterSearch}
|
||||||
onSearchChange={changeFilterSearch}
|
onSearchChange={changeFilterSearch}
|
||||||
onApply={() => { }}
|
onApply={() => { }}
|
||||||
tMenu={tMenu}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MenuSortingDrawer
|
|
||||||
visible={sortingModal}
|
|
||||||
onClose={toggleSortingModal}
|
|
||||||
sortings={sortings}
|
sortings={sortings}
|
||||||
activeIndex={activeSortingIndex}
|
activeSortIndex={activeSortingIndex}
|
||||||
onSelect={changeSorting}
|
onSortChange={changeSorting}
|
||||||
tMenu={tMenu}
|
tMenu={tMenu}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export interface ProductCategory {
|
|||||||
title: string;
|
title: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
shop: string;
|
shop: string;
|
||||||
avatarUrl: string;
|
avatarUrl: string | null;
|
||||||
order: number;
|
order: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProductVariant {
|
export interface ProductVariant {
|
||||||
@@ -51,10 +51,10 @@ export interface Product {
|
|||||||
deletedAt: string | null;
|
deletedAt: string | null;
|
||||||
shop: string;
|
shop: string;
|
||||||
category: ProductCategory;
|
category: ProductCategory;
|
||||||
attribute: string;
|
attribute: string | null;
|
||||||
title: string;
|
title: string;
|
||||||
desc: string;
|
desc: string;
|
||||||
order: number;
|
order: number | null;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
images: string[];
|
images: string[];
|
||||||
score: number | null;
|
score: number | null;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"Menu": {
|
"Menu": {
|
||||||
"MenuFilterDrawer": {
|
"MenuFilterDrawer": {
|
||||||
"Label": "فیلتر بر اساس",
|
"Label": "فیلتر و مرتبسازی",
|
||||||
"Heading": "فیلتر ها",
|
"Heading": "فیلتر و مرتبسازی",
|
||||||
"SelectContent": {
|
"SelectContent": {
|
||||||
"Label": "جستجو",
|
"Label": "جستجو",
|
||||||
"SearchLabel": "جستجو ...",
|
"SearchLabel": "جستجو ...",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user