merge filter and sort

This commit is contained in:
hamid zarghami
2026-02-14 11:02:09 +03:30
parent ebb3b7d079
commit a8deff9267
6 changed files with 88 additions and 91 deletions
@@ -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;
+11 -27
View File
@@ -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>
+4 -4
View File
@@ -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;
+2 -2
View File
@@ -1,8 +1,8 @@
{ {
"Menu": { "Menu": {
"MenuFilterDrawer": { "MenuFilterDrawer": {
"Label": "فیلتر بر اساس", "Label": "فیلتر و مرتب‌سازی",
"Heading": "فیلتر ها", "Heading": "فیلتر و مرتب‌سازی",
"SelectContent": { "SelectContent": {
"Label": "جستجو", "Label": "جستجو",
"SearchLabel": "جستجو ...", "SearchLabel": "جستجو ...",
+1 -1
View File
File diff suppressed because one or more lines are too long