diff --git a/src/app/[name]/(Main)/components/CategoryScroll.tsx b/src/app/[name]/(Main)/components/CategoryScroll.tsx new file mode 100644 index 0000000..9e71e1e --- /dev/null +++ b/src/app/[name]/(Main)/components/CategoryScroll.tsx @@ -0,0 +1,96 @@ +'use client'; + +import Image from "next/image"; +import clsx from "clsx"; +import HorizontalScrollView from "@/components/listview/HorizontalScrollView"; +import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer"; +import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer"; +import { Category } from "@/app/[name]/(Main)/types/Types"; + +type Variant = "large" | "small"; + +const variantConfig: Record< + Variant, + { + renderer: typeof CategoryItemRenderer; + imageSize: number; + } +> = { + large: { + renderer: CategoryItemRenderer, + imageSize: 32, + }, + small: { + renderer: CategorySmallItemRenderer, + imageSize: 24, + }, +}; + +type Props = { + categories: Category[]; + selectedCategory: string; + onSelect: (index: number) => void; + variant?: Variant; + className?: string; +}; + +const CategoryScroll = ({ + categories, + selectedCategory, + onSelect, + variant = "large", + className, +}: Props) => { + const { renderer: Renderer, imageSize } = variantConfig[variant]; + + const handleSelect = (index: number) => () => onSelect(index); + + return ( + + + category image + همه + + {categories.map((item, index) => { + const categoryIndex = index + 1; + const isSelected = String(categoryIndex) === selectedCategory; + + return ( + + category image + {item.title} + + ); + })} + + ); +}; + +export default CategoryScroll; + diff --git a/src/app/[name]/(Main)/components/MenuFilterDrawer.tsx b/src/app/[name]/(Main)/components/MenuFilterDrawer.tsx new file mode 100644 index 0000000..782cb62 --- /dev/null +++ b/src/app/[name]/(Main)/components/MenuFilterDrawer.tsx @@ -0,0 +1,118 @@ +'use client'; + +import React from "react"; +import Button from "@/components/button/PrimaryButton"; +import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet"; +import ComboBox, { ComboboxOption } from "@/components/combobox/Combobox"; +import { Switch } from "@/components/ui/switch"; +import { MedalStar } from "iconsax-react"; +import { TicketPercentIcon } from "lucide-react"; + +type MenuFilterDrawerProps = { + visible: boolean; + onClose: () => void; + selectedIngredients: string; + selectedDeliveryId: string; + onIngredientsChange: (value: string) => void; + onDeliveryChange: (value: string) => void; + tMenu: (key: string) => string; +}; + +const MenuFilterDrawer = ({ + visible, + onClose, + selectedIngredients, + selectedDeliveryId, + onIngredientsChange, + onDeliveryChange, + tMenu, +}: MenuFilterDrawerProps) => { + const contents: Array = [ + { + id: "0", + title: tMenu("MenuFilterDrawer.SelectContent.Options.Normal"), + label: "", + }, + { + id: "1", + title: tMenu("MenuFilterDrawer.SelectContent.Options.Vegan"), + label: "", + }, + ]; + + const shippings: Array = [ + { + id: "0", + title: tMenu("MenuFilterDrawer.SelectDelivery.Options.Courier"), + label: "", + }, + ]; + + const handleIngredientsChange = ( + e: React.MouseEvent, + index: number + ) => { + e.stopPropagation(); + onIngredientsChange(contents[index]?.id ?? contents[0].id); + }; + + const handleDeliveryChange = ( + e: React.MouseEvent, + index: number + ) => { + e.stopPropagation(); + onDeliveryChange(shippings[index]?.id ?? shippings[0].id); + }; + + return ( + +
+ + +
+ + + {tMenu("MenuFilterDrawer.PlusPoint")} + + +
+
+ + + {tMenu("MenuFilterDrawer.DiscountState")} + + +
+
+
+
+
+ +
+
+ +
+
+
+ ); +}; + +export default MenuFilterDrawer; diff --git a/src/app/[name]/(Main)/components/MenuSortingDrawer.tsx b/src/app/[name]/(Main)/components/MenuSortingDrawer.tsx new file mode 100644 index 0000000..5aa499f --- /dev/null +++ b/src/app/[name]/(Main)/components/MenuSortingDrawer.tsx @@ -0,0 +1,48 @@ +'use client'; + +import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet"; +import React from "react"; + +type MenuSortingDrawerProps = { + visible: boolean; + onClose: () => void; + sortings: string[]; + onSelect: (index: number) => void; + tMenu: (key: string) => string; +}; + +const MenuSortingDrawer = ({ + visible, + onClose, + sortings, + onSelect, + tMenu, +}: MenuSortingDrawerProps) => { + return ( + +
+ {sortings.map((value, index) => ( +
+
onSelect(index)} + className="text-sm2 font-normal cursor-pointer" + > + {tMenu(`MenuSortingDrawer.Options.${value}`)} +
+ {index < sortings.length - 1 && ( +
+ )} +
+ ))} +
+
+ ); +}; + +export default MenuSortingDrawer; + diff --git a/src/app/[name]/(Main)/hooks/useMenuData.ts b/src/app/[name]/(Main)/hooks/useMenuData.ts new file mode 100644 index 0000000..670dd34 --- /dev/null +++ b/src/app/[name]/(Main)/hooks/useMenuData.ts @@ -0,0 +1,21 @@ +import { useQuery } from "@tanstack/react-query"; +import * as api from "../service/MenuService"; +import { useParams } from "next/navigation"; + +export const useGetFoods = () => { + const { name } = useParams<{ name: string }>(); + return useQuery({ + queryKey: ["menu"], + queryFn: () => api.getFoods(name), + enabled: !!name, + }); +}; + +export const useGetCategories = () => { + const { name } = useParams<{ name: string }>(); + return useQuery({ + queryKey: ["categories"], + queryFn: () => api.getCategories(name), + enabled: !!name, + }); +}; diff --git a/src/app/[name]/(Main)/page.tsx b/src/app/[name]/(Main)/page.tsx index 8f4eaea..5d257d9 100644 --- a/src/app/[name]/(Main)/page.tsx +++ b/src/app/[name]/(Main)/page.tsx @@ -2,129 +2,20 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import SearchBox from "@/components/input/SearchBox"; -import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer"; -import HorizontalScrollView from "@/components/listview/HorizontalScrollView"; -import Image from "next/image"; import TextAlignIcon from "@/components/icons/TextAlignIcon"; import VerticalScrollView from "@/components/listview/VerticalScrollView"; import MenuItemRenderer from "@/components/listview/MenuItemRenderer"; -import React from "react"; import MenuItem from "@/components/listview/MenuItem"; -import Button from "@/components/button/PrimaryButton"; -import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet"; -import { Switch } from "@/components/ui/switch"; -import { TicketPercentIcon } from "lucide-react"; -import { Candle2, MedalStar } from 'iconsax-react' -import { useQueryState } from 'next-usequerystate' +import { Candle2 } from "iconsax-react"; +import { useQueryState } from "next-usequerystate"; import clsx from "clsx"; -import CategorySmallItemRenderer from "@/components/listview/CategorySmallItemRenderer"; import { motion } from "framer-motion"; import { useTranslation } from "react-i18next"; -import ComboBox, { ComboboxOption } from "@/components/combobox/Combobox"; import useToggle from "@/hooks/helpers/useToggle"; - -const categories = new Array(13).fill({ title: "خوراک", icon: "" }); - -const foods = [ - { - id: 1, - category: 0, - price: "69.000", - name: "کباب چوبی ژیوان )سیخ چوبی(", - contains: "فیله گوساله / سس سفید / قارچ و سیب زمینی/ پنیر پارمزان" - }, - { - id: 2, - category: 1, - price: "49.000", - name: "برگر گوشت گوساله", - contains: "گوشت گوساله / پنیر چدار / کاهو / گوجه" - }, - { - id: 3, - category: 2, - price: "29.000", - name: "ساندویچ مرغ", - contains: "مرغ / کاهو / گوجه / سس مایونز" - }, - { - id: 4, - category: 0, - price: "89.000", - name: "کباب ماهی", - contains: "ماهی / سس لیمو / قارچ و سیب زمینی" - }, - { - id: 5, - category: 1, - price: "39.000", - name: "سالاد فصل", - contains: "کاهو / گوجه / خیار / پنیر فتا" - }, - { - id: 6, - category: 3, - price: "59.000", - name: "پاستا گوشت", - contains: "گوشت گوساله / پاستا / سس گوجه" - }, - { - id: 7, - category: 2, - price: "49.000", - name: "ساندویچ تن ماهی", - contains: "تن ماهی / کاهو / گوجه / سس مایونز" - }, - { - id: 8, - category: 0, - price: "79.000", - name: "کباب گوشت گوسفندی", - contains: "گوشت گوسفندی / سس سفید / قارچ و سیب زمینی" - }, - { - id: 9, - category: 1, - price: "29.000", - name: "برگر مرغ", - contains: "مرغ / پنیر چدار / کاهو / گوجه" - }, - { - id: 10, - category: 3, - price: "69.000", - name: "سالاد میگو", - contains: "میگو / کاهو / گوجه / خیار" - }, - { - id: 11, - category: 2, - price: "39.000", - name: "ساندویچ شاورما", - contains: "مرغ / کاهو / گوجه / سس تارتار" - }, - { - id: 12, - category: 0, - price: "99.000", - name: "کباب گوشت بوقلمون", - contains: "گوشت بوقلمون / سس سفید / قارچ و سیب زمینی" - }, - { - id: 13, - category: 1, - price: "59.000", - name: "برگر ماهی", - contains: "ماهی / پنیر چدار / کاهو / گوجه" - }, - { - id: 14, - category: 3, - price: "49.000", - name: "سالاد مرغ", - contains: "مرغ / کاهو / گوجه / خیار" - } -]; +import CategoryScroll from "@/app/[name]/(Main)/components/CategoryScroll"; +import MenuFilterDrawer from "@/app/[name]/(Main)/components/MenuFilterDrawer"; +import MenuSortingDrawer from "@/app/[name]/(Main)/components/MenuSortingDrawer"; +import { useGetCategories, useGetFoods } from "./hooks/useMenuData"; const sortings = [ "PopularityDescending", @@ -135,18 +26,18 @@ const sortings = [ ] const MenuIndex = () => { + const { data: foodsData } = useGetFoods(); + const { data: categoriesData } = useGetCategories(); + + const foods = useMemo(() => { + const foodsList = foodsData?.data || []; + return foodsList; + }, [foodsData?.data]); + const categories = useMemo(() => categoriesData?.data || [], [categoriesData?.data]); const { t: tCommon } = useTranslation('common'); const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" }); - const contents: Array = [ - { id: '0', title: tMenu('MenuFilterDrawer.SelectContent.Options.Normal'), label: '' }, - { id: '1', title: tMenu('MenuFilterDrawer.SelectContent.Options.Vegan'), label: '' }, - ] - - const shippings: Array = [ - { id: '0', title: tMenu('MenuFilterDrawer.SelectDelivery.Options.Courier'), label: '' }, - ] const { state: filterModal, toggle: toggleFilterModal } = useToggle(); const { state: sortingModal, toggle: toggleSortingModal } = useToggle(); @@ -188,62 +79,54 @@ const MenuIndex = () => { setSearch(e.target.value); }, [setSearch]); - const updateCategory = useCallback((id: number) => { - setSelectedCategory(String(id)); + const updateCategory = useCallback((index: number) => { + setSelectedCategory(String(index)); }, [setSelectedCategory]); const changeSorting = (index: number) => { setSorting(() => String(index)); } - const changeSelectedIngredients = (e: React.MouseEvent, index: number) => { - setSelectedIngredients(() => contents[index]?.id || contents[0].id); - } + const changeSelectedIngredients = useCallback((value: string) => { + setSelectedIngredients(() => value); + }, [setSelectedIngredients]); - const changeSelectedDelivery = (e: React.MouseEvent, index: number) => { - setSelectedDeliveryId(() => contents[index]?.id || contents[0].id); - } + const changeSelectedDelivery = useCallback((value: string) => { + setSelectedDeliveryId(() => value); + }, [setSelectedDeliveryId]); const filteredReceiptItems = useMemo(() => { + if (!foods.length) return []; + const lowerSearch = search.toLowerCase(); + const selectedCatId = selectedCategory === '0' ? null : categories[+selectedCategory - 1]?.id; + return foods.filter( - (item) => - item.category === +selectedCategory && - item.name.toLowerCase().includes(lowerSearch) + (item) => { + const matchesCategory = selectedCategory === '0' || item.category === selectedCatId; + const itemName = item.name || item.title || item.foodName || ''; + const matchesSearch = !search || itemName.toLowerCase().includes(lowerSearch); + return matchesCategory && matchesSearch; + } ); - }, [selectedCategory, search]); + }, [selectedCategory, search, foods, categories]); return (
- - {categories.map((item, index) => ( - updateCategory(index)} - > - category image - {item.title} - - ))} - +
- {categories[+selectedCategory]?.title} + {selectedCategory === '0' ? 'همه' : categories[+selectedCategory - 1]?.title || ''}
- -
- - -
- - - {tMenu('MenuFilterDrawer.PlusPoint')} - - -
-
- - - {tMenu('MenuFilterDrawer.DiscountState')} - - -
-
-
-
-
- -
-
- -
-
-
+ - -
- {sortings.map((v, i) => { - return ( -
-
changeSorting(i)} className="text-sm2 font-normal cursor-pointer"> - {tMenu('MenuSortingDrawer.Options.' + v)} -
- {i < sortings.length - 1 &&
} -
- ) - })} -
-
+
); }; diff --git a/src/app/[name]/(Main)/service/MenuService.ts b/src/app/[name]/(Main)/service/MenuService.ts new file mode 100644 index 0000000..a10396b --- /dev/null +++ b/src/app/[name]/(Main)/service/MenuService.ts @@ -0,0 +1,21 @@ +import { api } from "@/lib/api/axiosInstance"; +import { + CategoriesResponse, + FoodsResponse, +} from "@/app/[name]/(Main)/types/Types"; + +export const getFoods = async (slug: string): Promise => { + const response = await api.get( + `/public/foods/restaurant/${slug}` + ); + return response.data; +}; + +export const getCategories = async ( + slug: string +): Promise => { + const response = await api.get( + `/public/categories/restaurant/${slug}` + ); + return response.data; +}; diff --git a/src/app/[name]/(Main)/types/Types.ts b/src/app/[name]/(Main)/types/Types.ts new file mode 100644 index 0000000..9faafd9 --- /dev/null +++ b/src/app/[name]/(Main)/types/Types.ts @@ -0,0 +1,35 @@ +export interface BaseResponse { + statusCode?: number; + success: boolean; + data: T; + message?: string; +} + +export interface Food { + id: string; + name?: string; + title?: string; + foodName?: string; + description?: string; + desc?: string; + content?: string; + price: number; + image?: string; + category?: string; + [key: string]: unknown; +} + +export type FoodsResponse = BaseResponse; + +export interface Category { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + title: string; + isActive: boolean; + restId: string; + avatarUrl: string | null; +} + +export type CategoriesResponse = BaseResponse; diff --git a/src/components/listview/MenuItem.tsx b/src/components/listview/MenuItem.tsx index c2cc935..b155e56 100644 --- a/src/components/listview/MenuItem.tsx +++ b/src/components/listview/MenuItem.tsx @@ -8,10 +8,15 @@ import { useReceiptStore } from "@/zustand/receiptStore"; import { motion } from "framer-motion"; interface MenuItemProps { food: { - id: number - name: string; - contains: string; - price: string; + id: string | number; + name?: string; + title?: string; + foodName?: string; + description?: string; + desc?: string; + content?: string; + price: number; + image?: string; }; } @@ -21,24 +26,35 @@ const MenuItem = ({ food }: MenuItemProps) => { const increment = useReceiptStore(state => state.increment); const decrement = useReceiptStore(state => state.decrement); + if (!food) return null; + + const foodName = food.name || food.title || food.foodName || 'بدون نام'; + const foodDescription = food.description || food.desc || food.content || ''; + return (
Food image
-
{food.name}
-
- {food.contains} +
+ {foodName}
+ {foodDescription && ( +
+ {foodDescription} +
+ )}
- {food.price} T + + {food.price ? food.price.toLocaleString('fa-IR') : '0'} T + void; - decrement: (id: number) => void; + increment: (id: string | number) => void; + decrement: (id: string | number) => void; clear: () => void; };