sort foods
This commit is contained in:
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,12 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
||||||
|
import clsx from "clsx";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
type MenuSortingDrawerProps = {
|
type MenuSortingDrawerProps = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
sortings: string[];
|
sortings: ReadonlyArray<string>;
|
||||||
|
activeIndex: number;
|
||||||
onSelect: (index: number) => void;
|
onSelect: (index: number) => void;
|
||||||
tMenu: (key: string) => string;
|
tMenu: (key: string) => string;
|
||||||
};
|
};
|
||||||
@@ -15,6 +17,7 @@ const MenuSortingDrawer = ({
|
|||||||
visible,
|
visible,
|
||||||
onClose,
|
onClose,
|
||||||
sortings,
|
sortings,
|
||||||
|
activeIndex,
|
||||||
onSelect,
|
onSelect,
|
||||||
tMenu,
|
tMenu,
|
||||||
}: MenuSortingDrawerProps) => {
|
}: MenuSortingDrawerProps) => {
|
||||||
@@ -25,20 +28,23 @@ const MenuSortingDrawer = ({
|
|||||||
inDelay={150}
|
inDelay={150}
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div className="px-8.5 py-10 justify-between">
|
<div className="px-8.5 py-10 flex flex-col gap-2">
|
||||||
{sortings.map((value, index) => (
|
{sortings.map((value, index) => {
|
||||||
<div key={value}>
|
const isActive = index === activeIndex;
|
||||||
<div
|
return (
|
||||||
|
<button
|
||||||
|
key={value}
|
||||||
|
type="button"
|
||||||
onClick={() => onSelect(index)}
|
onClick={() => onSelect(index)}
|
||||||
className="text-sm2 font-normal cursor-pointer"
|
className={clsx(
|
||||||
|
"flex items-center justify-between py-3 text-sm2 transition-colors",
|
||||||
|
isActive ? "text-primary font-semibold" : "text-foreground"
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{tMenu(`MenuSortingDrawer.Options.${value}`)}
|
<span>{tMenu(`MenuSortingDrawer.Options.${value}`)}</span>
|
||||||
</div>
|
</button>
|
||||||
{index < sortings.length - 1 && (
|
);
|
||||||
<hr className="border-white/40 dark:border-border mb-4 mt-4" />
|
})}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</AnimatedBottomSheet>
|
</AnimatedBottomSheet>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
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";
|
||||||
@@ -18,29 +17,18 @@ import MenuSortingDrawer from "@/app/[name]/(Main)/components/MenuSortingDrawer"
|
|||||||
import { useGetCategories, useGetFoods } from "./hooks/useMenuData";
|
import { useGetCategories, useGetFoods } from "./hooks/useMenuData";
|
||||||
import type { Food, Category } from "./types/Types";
|
import type { Food, Category } from "./types/Types";
|
||||||
|
|
||||||
const sortings = [
|
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
||||||
"PopularityDescending",
|
|
||||||
"DateDescending",
|
|
||||||
"PriceAscending",
|
|
||||||
"RateDescending",
|
|
||||||
"PriceDescending"
|
|
||||||
];
|
|
||||||
|
|
||||||
const MenuIndex = () => {
|
const MenuIndex = () => {
|
||||||
const { data: foodsData } = useGetFoods();
|
const { data: foodsData } = useGetFoods();
|
||||||
const { data: categoriesData } = useGetCategories();
|
const { data: categoriesData } = useGetCategories();
|
||||||
|
|
||||||
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
|
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
|
||||||
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
|
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
|
||||||
const { t: tCommon } = useTranslation('common');
|
const { t: tCommon } = useTranslation('common');
|
||||||
const { t: tMenu } = useTranslation('menu', {
|
const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" });
|
||||||
keyPrefix: "Menu"
|
|
||||||
});
|
|
||||||
|
|
||||||
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
||||||
const { state: sortingModal, toggle: toggleSortingModal } = useToggle();
|
const { state: sortingModal, toggle: toggleSortingModal } = useToggle();
|
||||||
|
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
|
||||||
const [, 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' });
|
||||||
const [selectedIngredients, setSelectedIngredients] = useQueryState('ingredients', { defaultValue: '0' });
|
const [selectedIngredients, setSelectedIngredients] = useQueryState('ingredients', { defaultValue: '0' });
|
||||||
@@ -81,9 +69,15 @@ const MenuIndex = () => {
|
|||||||
setSelectedCategory(String(index));
|
setSelectedCategory(String(index));
|
||||||
}, [setSelectedCategory]);
|
}, [setSelectedCategory]);
|
||||||
|
|
||||||
|
const sortingIndex = Number(sorting);
|
||||||
|
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 = (index: number) => {
|
||||||
setSorting(() => String(index));
|
setSorting(() => String(index));
|
||||||
}
|
toggleSortingModal();
|
||||||
|
};
|
||||||
|
|
||||||
const changeSelectedIngredients = useCallback((value: string) => {
|
const changeSelectedIngredients = useCallback((value: string) => {
|
||||||
setSelectedIngredients(() => value);
|
setSelectedIngredients(() => value);
|
||||||
@@ -95,22 +89,36 @@ const MenuIndex = () => {
|
|||||||
|
|
||||||
const filteredReceiptItems = useMemo(() => {
|
const filteredReceiptItems = useMemo(() => {
|
||||||
if (!foods.length) return [];
|
if (!foods.length) return [];
|
||||||
|
|
||||||
const lowerSearch = search.toLowerCase();
|
const lowerSearch = search.toLowerCase();
|
||||||
const selectedCatId = selectedCategory === '0' ? null : categories[+selectedCategory - 1]?.id;
|
const selectedCatId =
|
||||||
|
selectedCategory === "0" ? null : categories[+selectedCategory - 1]?.id;
|
||||||
|
|
||||||
return foods.filter(
|
const filtered = foods.filter((item) => {
|
||||||
(item) => {
|
const matchesCategory =
|
||||||
const matchesCategory =
|
selectedCategory === "0" || item.category?.id === selectedCatId;
|
||||||
selectedCategory === '0' || item.category?.id === selectedCatId;
|
const itemName = item.name || item.title || item.foodName || "";
|
||||||
const itemName = item.name || item.title || item.foodName || '';
|
const matchesSearch =
|
||||||
const matchesSearch = !search || itemName.toLowerCase().includes(lowerSearch);
|
!search || itemName.toLowerCase().includes(lowerSearch);
|
||||||
return matchesCategory && matchesSearch;
|
return matchesCategory && matchesSearch;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortingKey = sortings[Number(sorting)] || sortings[0];
|
||||||
|
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
|
switch (sortingKey) {
|
||||||
|
case "PopularityDescending":
|
||||||
|
return (b.points ?? 0) - (a.points ?? 0);
|
||||||
|
case "RateDescending":
|
||||||
|
return (b.rate ?? 0) - (a.rate ?? 0);
|
||||||
|
case "PriceAscending":
|
||||||
|
return (a.price ?? 0) - (b.price ?? 0);
|
||||||
|
case "PriceDescending":
|
||||||
|
return (b.price ?? 0) - (a.price ?? 0);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
}, [selectedCategory, search, foods, categories]);
|
}, [selectedCategory, search, foods, categories, sorting]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 items-center pt-8 mb-8" ref={wrapperRef}>
|
<div className="flex flex-col gap-4 items-center pt-8 mb-8" ref={wrapperRef}>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -132,8 +140,12 @@ const MenuIndex = () => {
|
|||||||
<Candle2 className="stroke-foreground" size={16} />
|
<Candle2 className="stroke-foreground" size={16} />
|
||||||
<span className="text-xs leading-5 font-medium">{tMenu('MenuFilterDrawer.Label')}</span>
|
<span className="text-xs leading-5 font-medium">{tMenu('MenuFilterDrawer.Label')}</span>
|
||||||
</button>
|
</button>
|
||||||
<button onClick={toggleSortingModal} className="rounded-lg h-8 bg-container p-1.5">
|
<button
|
||||||
|
onClick={toggleSortingModal}
|
||||||
|
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>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -183,6 +195,7 @@ const MenuIndex = () => {
|
|||||||
visible={sortingModal}
|
visible={sortingModal}
|
||||||
onClose={toggleSortingModal}
|
onClose={toggleSortingModal}
|
||||||
sortings={sortings}
|
sortings={sortings}
|
||||||
|
activeIndex={activeSortingIndex}
|
||||||
onSelect={changeSorting}
|
onSelect={changeSorting}
|
||||||
tMenu={tMenu}
|
tMenu={tMenu}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+10
-13
@@ -2,11 +2,12 @@ import PreferenceWrapper from '@/components/wrapper/PreferenceWrapper'
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { Metadata, Viewport } from 'next'
|
import type { Metadata, Viewport } from 'next'
|
||||||
|
|
||||||
|
type LayoutParams = { name: string }
|
||||||
|
|
||||||
export async function generateMetadata ({
|
export async function generateMetadata ({
|
||||||
params
|
params
|
||||||
}: {
|
}: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
params: Promise<LayoutParams>
|
||||||
params: any
|
|
||||||
}): Promise<Metadata> {
|
}): Promise<Metadata> {
|
||||||
const { name } = await params
|
const { name } = await params
|
||||||
return {
|
return {
|
||||||
@@ -16,21 +17,17 @@ export async function generateMetadata ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateViewport ({}: {
|
export async function generateViewport (): Promise<Viewport> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
params: any
|
|
||||||
}): Promise<Viewport> {
|
|
||||||
return {
|
return {
|
||||||
themeColor: '#F4F5F9'
|
themeColor: '#F4F5F9'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
export default function Layout ({
|
||||||
children: React.ReactElement
|
children
|
||||||
}
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
function layout ({ children }: Props) {
|
params: Promise<LayoutParams>
|
||||||
|
}): React.ReactNode {
|
||||||
return <PreferenceWrapper>{children}</PreferenceWrapper>
|
return <PreferenceWrapper>{children}</PreferenceWrapper>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default layout
|
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
export enum AUTH_STEP {
|
export enum AUTH_STEP {
|
||||||
ENTER_NUMBER = 0,
|
ENTER_NUMBER = 0,
|
||||||
ENTER_OTP = 1,
|
ENTER_OTP = 1,
|
||||||
|
ENTER_CREATE_PASSWORD = 2,
|
||||||
|
ENTER_CURRENT_PASSWORD = 3,
|
||||||
|
ENTER_RESET_PASSWORD = 4
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AUTH_PAGE_ELEMENT, AUTH_STEP } from '@/enums'
|
|||||||
import { LoginOTPRequestType } from '@/app/auth/login/types/Types'
|
import { LoginOTPRequestType } from '@/app/auth/login/types/Types'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
import type { ChangeEvent } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useFormik } from 'formik'
|
import { useFormik } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
@@ -15,9 +16,14 @@ import { toast } from '@/components/Toast'
|
|||||||
import StepOtp from './StepOtp'
|
import StepOtp from './StepOtp'
|
||||||
import { extractErrorMessage } from '@/lib/func'
|
import { extractErrorMessage } from '@/lib/func'
|
||||||
|
|
||||||
|
type StepEnterNumberProps = {
|
||||||
|
pending?: boolean
|
||||||
|
onChange?: (event: ChangeEvent<HTMLInputElement>) => void
|
||||||
|
value?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
function StepEnterNumber (_props: StepEnterNumberProps = {}) {
|
||||||
function StepEnterNumber() {
|
void _props
|
||||||
|
|
||||||
const { t } = useTranslation('auth')
|
const { t } = useTranslation('auth')
|
||||||
const { name } = useParams()
|
const { name } = useParams()
|
||||||
|
|||||||
@@ -29,7 +29,6 @@
|
|||||||
"Options": {
|
"Options": {
|
||||||
"PopularityDescending": "محبوب ترین",
|
"PopularityDescending": "محبوب ترین",
|
||||||
"RateDescending": "بالاترین امتیاز",
|
"RateDescending": "بالاترین امتیاز",
|
||||||
"DateDescending": "جدیدترین",
|
|
||||||
"PriceAscending": "ارزان ترین",
|
"PriceAscending": "ارزان ترین",
|
||||||
"PriceDescending": "گران ترین"
|
"PriceDescending": "گران ترین"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user