remove ssr
This commit is contained in:
@@ -3,8 +3,6 @@ import { type FC } from 'react'
|
|||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { CloseCircle } from 'iconsax-react'
|
import { CloseCircle } from 'iconsax-react'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
|
|
||||||
const Game2048Page: FC = () => {
|
const Game2048Page: FC = () => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,5 @@
|
|||||||
import React from 'react'
|
export default function AboutLayout({
|
||||||
import { Metadata } from 'next';
|
children,
|
||||||
import { getAboutData } from '@/lib/api/info/getAboutData';
|
}: Readonly<{ children: React.ReactNode }>) {
|
||||||
|
return <>{children}</>;
|
||||||
export type AboutPageProps = { name: string }
|
|
||||||
|
|
||||||
type Params = { name: string };
|
|
||||||
|
|
||||||
export const revalidate = 60
|
|
||||||
|
|
||||||
export const dynamicParams = false // or false, to 404 on unknown paths
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
|
||||||
const { name } = await params;
|
|
||||||
const data = await getAboutData(name);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: data.metadata.title || `About | ${name}`,
|
|
||||||
description: data.metadata.description || `This is the about-us page for ${name}.`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
return [
|
|
||||||
{ name: 'zhivan' },
|
|
||||||
{ name: 'boote' },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function layout({ children }: Readonly<{ children: React.ReactNode; params: Promise<Params> }>) {
|
|
||||||
return (
|
|
||||||
<>{children}</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default layout
|
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
import React from 'react'
|
'use client';
|
||||||
import AboutPage from './AboutPage'
|
|
||||||
|
|
||||||
const page = () => {
|
export { default } from './AboutPage';
|
||||||
return (
|
|
||||||
<AboutPage />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default page
|
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
'use client';
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import SearchBox from "@/components/input/SearchBox";
|
||||||
|
import TextAlignIcon from "@/components/icons/TextAlignIcon";
|
||||||
|
import VerticalScrollView from "@/components/listview/VerticalScrollView";
|
||||||
|
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
||||||
|
import MenuItem from "@/components/listview/MenuItem";
|
||||||
|
import { Candle2 } from "iconsax-react";
|
||||||
|
import { useQueryState } from "next-usequerystate";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import useToggle from "@/hooks/helpers/useToggle";
|
||||||
|
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 MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton";
|
||||||
|
import { useGetCategories, useGetFoods } from "../hooks/useMenuData";
|
||||||
|
import type { Food, Category } from "../types/Types";
|
||||||
|
|
||||||
|
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
||||||
|
|
||||||
|
const MenuIndex = () => {
|
||||||
|
const { data: foodsData, isLoading: foodsLoading } = useGetFoods();
|
||||||
|
const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories();
|
||||||
|
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
|
||||||
|
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
|
||||||
|
|
||||||
|
const isLoading = foodsLoading || categoriesLoading;
|
||||||
|
const { t: tCommon } = useTranslation('common');
|
||||||
|
const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" });
|
||||||
|
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
||||||
|
const { state: sortingModal, toggle: toggleSortingModal } = useToggle();
|
||||||
|
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
|
||||||
|
const [search, setSearch] = useQueryState("q", { defaultValue: '' });
|
||||||
|
const [selectedCategory, setSelectedCategory] = useQueryState('category', { defaultValue: '0' });
|
||||||
|
const [selectedIngredients, setSelectedIngredients] = useQueryState('ingredients', { defaultValue: '' });
|
||||||
|
const [selectedDeliveryId, setSelectedDeliveryId] = useQueryState('delivery', { defaultValue: '0' });
|
||||||
|
const smallCategoriesRef: React.RefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
const wrapperRef: React.RefObject<HTMLDivElement | null> = useRef(null);
|
||||||
|
const [smallCategoriesVisible, setSmallCategoriesVisibility] = useState(false);
|
||||||
|
const [isInitialMount, setIsInitialMount] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialMount) {
|
||||||
|
setSearch(null);
|
||||||
|
setSelectedIngredients(null);
|
||||||
|
setSelectedDeliveryId(null);
|
||||||
|
setSorting(null);
|
||||||
|
setIsInitialMount(false);
|
||||||
|
}
|
||||||
|
}, [isInitialMount, setSearch, setSelectedIngredients, setSelectedDeliveryId, setSorting]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('selectedCategory', selectedCategory);
|
||||||
|
|
||||||
|
if (categoriesData?.data && selectedCategory === '0') {
|
||||||
|
setSelectedCategory(categoriesData?.data?.[0]?.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedCategory, categoriesData])
|
||||||
|
|
||||||
|
|
||||||
|
const onScroll = useCallback(() => {
|
||||||
|
if (!wrapperRef?.current?.parentElement?.parentElement?.scrollTop || !smallCategoriesRef.current?.offsetTop) return;
|
||||||
|
|
||||||
|
if (wrapperRef.current.parentElement?.parentElement.scrollTop >= smallCategoriesRef.current.offsetTop) {
|
||||||
|
setSmallCategoriesVisibility(true);
|
||||||
|
} else {
|
||||||
|
setSmallCategoriesVisibility(false);
|
||||||
|
}
|
||||||
|
}, [wrapperRef, smallCategoriesRef]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wrapperRef.current) return;
|
||||||
|
|
||||||
|
const parent = wrapperRef.current.parentElement?.parentElement;
|
||||||
|
if (!parent) return;
|
||||||
|
|
||||||
|
parent.addEventListener('scroll', onScroll);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
parent.removeEventListener('scroll', onScroll);
|
||||||
|
}
|
||||||
|
}, [onScroll]);
|
||||||
|
|
||||||
|
|
||||||
|
const updateSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
}, [setSearch]);
|
||||||
|
|
||||||
|
const updateCategory = useCallback((categoryId: string) => {
|
||||||
|
setSelectedCategory(categoryId);
|
||||||
|
}, [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) => {
|
||||||
|
setSorting(() => String(index));
|
||||||
|
toggleSortingModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeSelectedIngredients = useCallback((value: string) => {
|
||||||
|
setSelectedIngredients(value);
|
||||||
|
}, [setSelectedIngredients]);
|
||||||
|
|
||||||
|
const changeSelectedDelivery = useCallback((value: string) => {
|
||||||
|
setSelectedDeliveryId(() => value);
|
||||||
|
}, [setSelectedDeliveryId]);
|
||||||
|
|
||||||
|
const filteredReceiptItems = useMemo(() => {
|
||||||
|
if (!foods.length) return [];
|
||||||
|
const lowerSearch = search.toLowerCase();
|
||||||
|
const lowerIngredients = selectedIngredients ? selectedIngredients.toLowerCase() : "";
|
||||||
|
const selectedCatId = selectedCategory === '0' ? null : selectedCategory;
|
||||||
|
|
||||||
|
const filtered = foods.filter((item) => {
|
||||||
|
const matchesCategory = !selectedCatId || item.category === selectedCatId;
|
||||||
|
const itemName = item.title;
|
||||||
|
const matchesSearch =
|
||||||
|
!search || itemName.toLowerCase().includes(lowerSearch);
|
||||||
|
|
||||||
|
const matchesIngredients = !selectedIngredients ||
|
||||||
|
(() => {
|
||||||
|
if (item.content && Array.isArray(item.content) && item.content.length > 0) {
|
||||||
|
return item.content.some((content: string) =>
|
||||||
|
content.toLowerCase().includes(lowerIngredients)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return item.desc.toLowerCase().includes(lowerIngredients);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const matchesDelivery = selectedDeliveryId === "0" ||
|
||||||
|
(selectedDeliveryId === "pickup" && item.pickupServe) ||
|
||||||
|
(selectedDeliveryId === "inPlace" && item.inPlaceServe);
|
||||||
|
|
||||||
|
return matchesCategory && matchesSearch && matchesIngredients && matchesDelivery;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortingKey = sortings[Number(sorting)] || sortings[0];
|
||||||
|
|
||||||
|
return [...filtered].sort((a, b) => {
|
||||||
|
switch (sortingKey) {
|
||||||
|
case "PopularityDescending":
|
||||||
|
case "RateDescending":
|
||||||
|
return 0;
|
||||||
|
case "PriceAscending":
|
||||||
|
return (a.price ?? 0) - (b.price ?? 0);
|
||||||
|
case "PriceDescending":
|
||||||
|
return (b.price ?? 0) - (a.price ?? 0);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [selectedCategory, search, selectedIngredients, selectedDeliveryId, foods, sorting]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <MenuSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 items-center pt-8 mb-8" ref={wrapperRef}>
|
||||||
|
<div className="w-full">
|
||||||
|
<SearchBox value={search} placeholder={tCommon('SearchPlaceholder')} onChange={updateSearch} />
|
||||||
|
<CategoryScroll
|
||||||
|
categories={categories}
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
onSelect={updateCategory}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="w-full">
|
||||||
|
<div className="flex gap-2 justify-between items-center relative" ref={smallCategoriesRef} >
|
||||||
|
<span className="sm:text-base text-sm font-medium">
|
||||||
|
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
|
||||||
|
</span>
|
||||||
|
<div className="inline-flex min-w-[247px] gap-2 justify-around 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
|
||||||
|
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" />
|
||||||
|
<span className="text-xs leading-5 font-medium">{activeSortingLabel}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ y: smallCategoriesVisible ? 0 : -50, opacity: smallCategoriesVisible ? 1 : 0 }}
|
||||||
|
animate={{ y: smallCategoriesVisible ? 0 : -50, opacity: smallCategoriesVisible ? 1 : 0 }}
|
||||||
|
transition={{ duration: .1 }}
|
||||||
|
className={clsx(
|
||||||
|
'fixed left-0 z-10 top-0 px-4 pt-16 bg-[#F4F5F9CC] dark:bg-background/70 backdrop-blur-[44px] right-0 xl:pr-72 xl:pt-20',
|
||||||
|
``
|
||||||
|
)}>
|
||||||
|
<CategoryScroll
|
||||||
|
categories={categories}
|
||||||
|
selectedCategory={selectedCategory}
|
||||||
|
onSelect={updateCategory}
|
||||||
|
variant="small"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<VerticalScrollView className="mt-5! overflow-y-auto h-full">
|
||||||
|
{filteredReceiptItems.length === 0 ? (
|
||||||
|
<div className="text-center text-foreground/60 py-8">
|
||||||
|
{foodsData ? 'غذایی یافت نشد' : 'در حال بارگذاری...'}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
filteredReceiptItems.map((food) => (
|
||||||
|
<MenuItemRenderer key={food.id}>
|
||||||
|
<MenuItem food={food} />
|
||||||
|
</MenuItemRenderer>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</VerticalScrollView>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<MenuFilterDrawer
|
||||||
|
visible={filterModal}
|
||||||
|
onClose={toggleFilterModal}
|
||||||
|
selectedIngredients={selectedIngredients}
|
||||||
|
selectedDeliveryId={selectedDeliveryId}
|
||||||
|
onIngredientsChange={changeSelectedIngredients}
|
||||||
|
onDeliveryChange={changeSelectedDelivery}
|
||||||
|
onApply={() => { }}
|
||||||
|
tMenu={tMenu}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MenuSortingDrawer
|
||||||
|
visible={sortingModal}
|
||||||
|
onClose={toggleSortingModal}
|
||||||
|
sortings={sortings}
|
||||||
|
activeIndex={activeSortingIndex}
|
||||||
|
onSelect={changeSorting}
|
||||||
|
tMenu={tMenu}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuIndex;
|
||||||
+38
-245
@@ -1,253 +1,46 @@
|
|||||||
'use client';
|
import type { Metadata, Viewport } from "next";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { notFound } from "next/navigation";
|
||||||
import SearchBox from "@/components/input/SearchBox";
|
import { getRestaurant } from "@/app/[name]/lib/getRestaurant";
|
||||||
import TextAlignIcon from "@/components/icons/TextAlignIcon";
|
import {
|
||||||
import VerticalScrollView from "@/components/listview/VerticalScrollView";
|
buildRestaurantMetadata,
|
||||||
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
buildRestaurantViewport,
|
||||||
import MenuItem from "@/components/listview/MenuItem";
|
} from "@/app/[name]/lib/restaurantMetadata";
|
||||||
import { Candle2 } from "iconsax-react";
|
import MenuIndex from "./components/MenuIndex";
|
||||||
import { useQueryState } from "next-usequerystate";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import useToggle from "@/hooks/helpers/useToggle";
|
|
||||||
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 MenuSkeleton from "@/app/[name]/(Main)/components/MenuSkeleton";
|
|
||||||
import { useGetCategories, useGetFoods } from "./hooks/useMenuData";
|
|
||||||
import type { Food, Category } from "./types/Types";
|
|
||||||
|
|
||||||
const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const;
|
type PageParams = { name: string };
|
||||||
|
|
||||||
const MenuIndex = () => {
|
export async function generateMetadata({
|
||||||
const { data: foodsData, isLoading: foodsLoading } = useGetFoods();
|
params,
|
||||||
const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories();
|
}: {
|
||||||
const foods = useMemo<Food[]>(() => foodsData?.data || [], [foodsData?.data]);
|
params: Promise<PageParams>;
|
||||||
const categories = useMemo<Category[]>(() => categoriesData?.data || [], [categoriesData?.data]);
|
}): Promise<Metadata> {
|
||||||
|
const { name } = await params;
|
||||||
const isLoading = foodsLoading || categoriesLoading;
|
return buildRestaurantMetadata(name);
|
||||||
const { t: tCommon } = useTranslation('common');
|
|
||||||
const { t: tMenu } = useTranslation('menu', { keyPrefix: "Menu" });
|
|
||||||
const { state: filterModal, toggle: toggleFilterModal } = useToggle();
|
|
||||||
const { state: sortingModal, toggle: toggleSortingModal } = useToggle();
|
|
||||||
const [sorting, setSorting] = useQueryState('sortBy', { defaultValue: '0' });
|
|
||||||
const [search, setSearch] = useQueryState("q", { defaultValue: '' });
|
|
||||||
const [selectedCategory, setSelectedCategory] = useQueryState('category', { defaultValue: '0' });
|
|
||||||
const [selectedIngredients, setSelectedIngredients] = useQueryState('ingredients', { defaultValue: '' });
|
|
||||||
const [selectedDeliveryId, setSelectedDeliveryId] = useQueryState('delivery', { defaultValue: '0' });
|
|
||||||
const smallCategoriesRef: React.RefObject<HTMLDivElement | null> = useRef(null);
|
|
||||||
const wrapperRef: React.RefObject<HTMLDivElement | null> = useRef(null);
|
|
||||||
const [smallCategoriesVisible, setSmallCategoriesVisibility] = useState(false);
|
|
||||||
const [isInitialMount, setIsInitialMount] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isInitialMount) {
|
|
||||||
setSearch(null);
|
|
||||||
// category را نگه میداریم تا در بک زدن حفظ شود
|
|
||||||
// setSelectedCategory(null);
|
|
||||||
setSelectedIngredients(null);
|
|
||||||
setSelectedDeliveryId(null);
|
|
||||||
setSorting(null);
|
|
||||||
setIsInitialMount(false);
|
|
||||||
}
|
|
||||||
}, [isInitialMount, setSearch, setSelectedIngredients, setSelectedDeliveryId, setSorting]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log('selectedCategory', selectedCategory);
|
|
||||||
|
|
||||||
if (categoriesData?.data && selectedCategory === '0') {
|
|
||||||
setSelectedCategory(categoriesData?.data?.[0]?.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
export async function generateViewport({
|
||||||
}, [selectedCategory, categoriesData])
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<PageParams>;
|
||||||
const onScroll = useCallback(() => {
|
}): Promise<Viewport> {
|
||||||
if (!wrapperRef?.current?.parentElement?.parentElement?.scrollTop || !smallCategoriesRef.current?.offsetTop) return;
|
const { name } = await params;
|
||||||
|
return buildRestaurantViewport(name);
|
||||||
if (wrapperRef.current.parentElement?.parentElement.scrollTop >= smallCategoriesRef.current.offsetTop) {
|
|
||||||
setSmallCategoriesVisibility(true);
|
|
||||||
} else {
|
|
||||||
setSmallCategoriesVisibility(false);
|
|
||||||
}
|
|
||||||
}, [wrapperRef, smallCategoriesRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!wrapperRef.current) return;
|
|
||||||
|
|
||||||
const parent = wrapperRef.current.parentElement?.parentElement;
|
|
||||||
if (!parent) return;
|
|
||||||
|
|
||||||
parent.addEventListener('scroll', onScroll);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
parent.removeEventListener('scroll', onScroll);
|
|
||||||
}
|
|
||||||
}, [onScroll]);
|
|
||||||
|
|
||||||
|
|
||||||
const updateSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setSearch(e.target.value);
|
|
||||||
}, [setSearch]);
|
|
||||||
|
|
||||||
const updateCategory = useCallback((categoryId: string) => {
|
|
||||||
setSelectedCategory(categoryId);
|
|
||||||
}, [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) => {
|
|
||||||
setSorting(() => String(index));
|
|
||||||
toggleSortingModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeSelectedIngredients = useCallback((value: string) => {
|
|
||||||
setSelectedIngredients(value);
|
|
||||||
}, [setSelectedIngredients]);
|
|
||||||
|
|
||||||
const changeSelectedDelivery = useCallback((value: string) => {
|
|
||||||
setSelectedDeliveryId(() => value);
|
|
||||||
}, [setSelectedDeliveryId]);
|
|
||||||
|
|
||||||
const filteredReceiptItems = useMemo(() => {
|
|
||||||
if (!foods.length) return [];
|
|
||||||
const lowerSearch = search.toLowerCase();
|
|
||||||
const lowerIngredients = selectedIngredients ? selectedIngredients.toLowerCase() : "";
|
|
||||||
const selectedCatId = selectedCategory === '0' ? null : selectedCategory;
|
|
||||||
|
|
||||||
const filtered = foods.filter((item) => {
|
|
||||||
const matchesCategory = !selectedCatId || item.category === selectedCatId;
|
|
||||||
const itemName = item.title;
|
|
||||||
const matchesSearch =
|
|
||||||
!search || itemName.toLowerCase().includes(lowerSearch);
|
|
||||||
|
|
||||||
const matchesIngredients = !selectedIngredients ||
|
|
||||||
(() => {
|
|
||||||
// ابتدا در محتویات جستجو میکنیم
|
|
||||||
if (item.content && Array.isArray(item.content) && item.content.length > 0) {
|
|
||||||
return item.content.some((content: string) =>
|
|
||||||
content.toLowerCase().includes(lowerIngredients)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return item.desc.toLowerCase().includes(lowerIngredients);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const matchesDelivery = selectedDeliveryId === "0" ||
|
|
||||||
(selectedDeliveryId === "pickup" && item.pickupServe) ||
|
|
||||||
(selectedDeliveryId === "inPlace" && item.inPlaceServe);
|
|
||||||
|
|
||||||
return matchesCategory && matchesSearch && matchesIngredients && matchesDelivery;
|
|
||||||
});
|
|
||||||
|
|
||||||
const sortingKey = sortings[Number(sorting)] || sortings[0];
|
|
||||||
|
|
||||||
return [...filtered].sort((a, b) => {
|
|
||||||
switch (sortingKey) {
|
|
||||||
case "PopularityDescending":
|
|
||||||
case "RateDescending":
|
|
||||||
return 0;
|
|
||||||
case "PriceAscending":
|
|
||||||
return (a.price ?? 0) - (b.price ?? 0);
|
|
||||||
case "PriceDescending":
|
|
||||||
return (b.price ?? 0) - (a.price ?? 0);
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [selectedCategory, search, selectedIngredients, selectedDeliveryId, foods, sorting]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <MenuSkeleton />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
export default async function Page({
|
||||||
<div className="flex flex-col gap-4 items-center pt-8 mb-8" ref={wrapperRef}>
|
params,
|
||||||
<div className="w-full">
|
}: {
|
||||||
<SearchBox value={search} placeholder={tCommon('SearchPlaceholder')} onChange={updateSearch} />
|
params: Promise<PageParams>;
|
||||||
<CategoryScroll
|
}) {
|
||||||
categories={categories}
|
const { name } = await params;
|
||||||
selectedCategory={selectedCategory}
|
|
||||||
onSelect={updateCategory}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="w-full">
|
try {
|
||||||
<div className="flex gap-2 justify-between items-center relative" ref={smallCategoriesRef} >
|
await getRestaurant(name);
|
||||||
<span className="sm:text-base text-sm font-medium">
|
} catch (error) {
|
||||||
{selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''}
|
if (error instanceof Error && error.message === "RESTAURANT_NOT_FOUND") {
|
||||||
</span>
|
notFound();
|
||||||
<div className="inline-flex min-w-[247px] gap-2 justify-around 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
|
|
||||||
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" />
|
|
||||||
<span className="text-xs leading-5 font-medium">{activeSortingLabel}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<motion.div
|
return <MenuIndex />;
|
||||||
initial={{ y: smallCategoriesVisible ? 0 : -50, opacity: smallCategoriesVisible ? 1 : 0 }}
|
}
|
||||||
animate={{ y: smallCategoriesVisible ? 0 : -50, opacity: smallCategoriesVisible ? 1 : 0 }}
|
|
||||||
transition={{ duration: .1 }}
|
|
||||||
className={clsx(
|
|
||||||
'fixed left-0 z-10 top-0 px-4 pt-16 bg-[#F4F5F9CC] dark:bg-background/70 backdrop-blur-[44px] right-0 xl:pr-72 xl:pt-20',
|
|
||||||
``
|
|
||||||
)}>
|
|
||||||
<CategoryScroll
|
|
||||||
categories={categories}
|
|
||||||
selectedCategory={selectedCategory}
|
|
||||||
onSelect={updateCategory}
|
|
||||||
variant="small"
|
|
||||||
/>
|
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<VerticalScrollView className="mt-5! overflow-y-auto h-full">
|
|
||||||
{filteredReceiptItems.length === 0 ? (
|
|
||||||
<div className="text-center text-foreground/60 py-8">
|
|
||||||
{foodsData ? 'غذایی یافت نشد' : 'در حال بارگذاری...'}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
filteredReceiptItems.map((food) => (
|
|
||||||
<MenuItemRenderer key={food.id}>
|
|
||||||
<MenuItem food={food} />
|
|
||||||
</MenuItemRenderer>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</VerticalScrollView>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<MenuFilterDrawer
|
|
||||||
visible={filterModal}
|
|
||||||
onClose={toggleFilterModal}
|
|
||||||
selectedIngredients={selectedIngredients}
|
|
||||||
selectedDeliveryId={selectedDeliveryId}
|
|
||||||
onIngredientsChange={changeSelectedIngredients}
|
|
||||||
onDeliveryChange={changeSelectedDelivery}
|
|
||||||
onApply={() => { }}
|
|
||||||
tMenu={tMenu}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MenuSortingDrawer
|
|
||||||
visible={sortingModal}
|
|
||||||
onClose={toggleSortingModal}
|
|
||||||
sortings={sortings}
|
|
||||||
activeIndex={activeSortingIndex}
|
|
||||||
onSelect={changeSorting}
|
|
||||||
tMenu={tMenu}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MenuIndex;
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ActiveChecker from "@/components/ActiveChecker";
|
||||||
|
import CartChecker from "@/components/CartChecker";
|
||||||
|
import RestaurantHeadManager from "@/components/RestaurantHeadManager";
|
||||||
|
import RestaurantNotFoundGuard from "@/components/RestaurantNotFoundGuard";
|
||||||
|
import PreferenceWrapper from "@/components/wrapper/PreferenceWrapper";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RestaurantLayoutClient({ children }: Props) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RestaurantHeadManager />
|
||||||
|
<RestaurantNotFoundGuard>
|
||||||
|
<PreferenceWrapper>{children}</PreferenceWrapper>
|
||||||
|
</RestaurantNotFoundGuard>
|
||||||
|
<CartChecker />
|
||||||
|
<ActiveChecker />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
+5
-110
@@ -1,114 +1,9 @@
|
|||||||
import PreferenceWrapper from '@/components/wrapper/PreferenceWrapper'
|
import RestaurantLayoutClient from "./RestaurantLayoutClient";
|
||||||
import React from 'react'
|
|
||||||
import type { Metadata, Viewport } from 'next'
|
|
||||||
import { getRestaurant } from './lib/getRestaurant'
|
|
||||||
import { notFound } from 'next/navigation'
|
|
||||||
import CartChecker from '@/components/CartChecker'
|
|
||||||
import ActiveChecker from '@/components/ActiveChecker'
|
|
||||||
import { PWA_ICON_192, PWA_ICON_512 } from '@/lib/helpers/pwaIcons'
|
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export default function Layout({
|
||||||
export const revalidate = 0
|
|
||||||
|
|
||||||
type LayoutParams = { name: string }
|
|
||||||
|
|
||||||
export async function generateMetadata({
|
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: Promise<LayoutParams>
|
|
||||||
}): Promise<Metadata> {
|
|
||||||
const { name } = await params
|
|
||||||
const defaultIcon = PWA_ICON_192
|
|
||||||
const defaultIcon512 = PWA_ICON_512
|
|
||||||
|
|
||||||
try {
|
|
||||||
const restaurant = await getRestaurant(name)
|
|
||||||
const title = restaurant.seoTitle || restaurant.name
|
|
||||||
const logo = restaurant.logo
|
|
||||||
|
|
||||||
let iconUrl = defaultIcon
|
|
||||||
let icon192Url = defaultIcon
|
|
||||||
let icon512Url = defaultIcon512
|
|
||||||
|
|
||||||
if (logo && logo.trim() !== "") {
|
|
||||||
iconUrl = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`
|
|
||||||
icon192Url = iconUrl
|
|
||||||
icon512Url = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=512`
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
description: restaurant.seoDescription || restaurant.description || `منوی ${restaurant.name}`,
|
|
||||||
manifest: `/${name}/manifest.webmanifest`,
|
|
||||||
icons: {
|
|
||||||
icon: [
|
|
||||||
{ url: iconUrl },
|
|
||||||
{ url: icon192Url, sizes: "192x192", type: "image/png" },
|
|
||||||
{ url: icon512Url, sizes: "512x512", type: "image/png" },
|
|
||||||
],
|
|
||||||
shortcut: iconUrl,
|
|
||||||
apple: iconUrl,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
title: `${name} - Dashboard`,
|
|
||||||
description: `PWA dashboard for ${name}`,
|
|
||||||
manifest: `/${name}/manifest.webmanifest`,
|
|
||||||
icons: {
|
|
||||||
icon: [
|
|
||||||
{ url: PWA_ICON_192 },
|
|
||||||
{ url: PWA_ICON_192, sizes: "192x192", type: "image/png" },
|
|
||||||
{ url: PWA_ICON_512, sizes: "512x512", type: "image/png" },
|
|
||||||
],
|
|
||||||
shortcut: PWA_ICON_192,
|
|
||||||
apple: PWA_ICON_192,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function generateViewport({
|
|
||||||
params
|
|
||||||
}: {
|
|
||||||
params: Promise<LayoutParams>
|
|
||||||
}): Promise<Viewport> {
|
|
||||||
try {
|
|
||||||
const { name } = await params
|
|
||||||
const restaurant = await getRestaurant(name)
|
|
||||||
|
|
||||||
return {
|
|
||||||
themeColor: restaurant.menuColor || '#F4F5F9'
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
themeColor: '#F4F5F9'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function Layout({
|
|
||||||
children,
|
children,
|
||||||
params
|
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode;
|
||||||
params: Promise<LayoutParams>
|
}) {
|
||||||
}): Promise<React.ReactNode> {
|
return <RestaurantLayoutClient>{children}</RestaurantLayoutClient>;
|
||||||
const { name } = await params
|
|
||||||
|
|
||||||
try {
|
|
||||||
await getRestaurant(name)
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message === 'RESTAURANT_NOT_FOUND') {
|
|
||||||
notFound()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PreferenceWrapper>{children}</PreferenceWrapper>
|
|
||||||
<CartChecker />
|
|
||||||
<ActiveChecker />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type { Metadata, Viewport } from "next";
|
||||||
|
import { PWA_ICON_192, PWA_ICON_512 } from "@/lib/helpers/pwaIcons";
|
||||||
|
import { getRestaurant } from "./getRestaurant";
|
||||||
|
|
||||||
|
export async function buildRestaurantMetadata(name: string): Promise<Metadata> {
|
||||||
|
const defaultIcon = PWA_ICON_192;
|
||||||
|
const defaultIcon512 = PWA_ICON_512;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const restaurant = await getRestaurant(name);
|
||||||
|
const title = restaurant.seoTitle || restaurant.name;
|
||||||
|
const logo = restaurant.logo;
|
||||||
|
|
||||||
|
let iconUrl = defaultIcon;
|
||||||
|
let icon192Url = defaultIcon;
|
||||||
|
let icon512Url = defaultIcon512;
|
||||||
|
|
||||||
|
if (logo && logo.trim() !== "") {
|
||||||
|
iconUrl = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`;
|
||||||
|
icon192Url = iconUrl;
|
||||||
|
icon512Url = `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=512`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description:
|
||||||
|
restaurant.seoDescription ||
|
||||||
|
restaurant.description ||
|
||||||
|
`منوی ${restaurant.name}`,
|
||||||
|
manifest: `/${name}/manifest.webmanifest`,
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: iconUrl },
|
||||||
|
{ url: icon192Url, sizes: "192x192", type: "image/png" },
|
||||||
|
{ url: icon512Url, sizes: "512x512", type: "image/png" },
|
||||||
|
],
|
||||||
|
shortcut: iconUrl,
|
||||||
|
apple: iconUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
title: `${name} - Dashboard`,
|
||||||
|
description: `PWA dashboard for ${name}`,
|
||||||
|
manifest: `/${name}/manifest.webmanifest`,
|
||||||
|
icons: {
|
||||||
|
icon: [
|
||||||
|
{ url: PWA_ICON_192 },
|
||||||
|
{ url: PWA_ICON_192, sizes: "192x192", type: "image/png" },
|
||||||
|
{ url: PWA_ICON_512, sizes: "512x512", type: "image/png" },
|
||||||
|
],
|
||||||
|
shortcut: PWA_ICON_192,
|
||||||
|
apple: PWA_ICON_192,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buildRestaurantViewport(name: string): Promise<Viewport> {
|
||||||
|
try {
|
||||||
|
const restaurant = await getRestaurant(name);
|
||||||
|
return {
|
||||||
|
themeColor: restaurant.menuColor || "#F4F5F9",
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
themeColor: "#F4F5F9",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
|
||||||
|
import { PWA_ICON_192 } from "@/lib/helpers/pwaIcons";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
function upsertMeta(name: string, content: string, attribute: "name" | "property" = "name") {
|
||||||
|
let element = document.querySelector(`meta[${attribute}="${name}"]`);
|
||||||
|
if (!element) {
|
||||||
|
element = document.createElement("meta");
|
||||||
|
element.setAttribute(attribute, name);
|
||||||
|
document.head.appendChild(element);
|
||||||
|
}
|
||||||
|
element.setAttribute("content", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertLink(rel: string, href: string) {
|
||||||
|
let element = document.querySelector(`link[rel="${rel}"]`) as HTMLLinkElement | null;
|
||||||
|
if (!element) {
|
||||||
|
element = document.createElement("link");
|
||||||
|
element.rel = rel;
|
||||||
|
document.head.appendChild(element);
|
||||||
|
}
|
||||||
|
element.href = href;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RestaurantHeadManager() {
|
||||||
|
const { name } = useParams<{ name: string }>();
|
||||||
|
const { data } = useGetAbout();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!name || !data?.data) return;
|
||||||
|
|
||||||
|
const restaurant = data.data;
|
||||||
|
document.title = restaurant.seoTitle || restaurant.name;
|
||||||
|
|
||||||
|
upsertMeta(
|
||||||
|
"description",
|
||||||
|
restaurant.seoDescription ||
|
||||||
|
restaurant.description ||
|
||||||
|
`منوی ${restaurant.name}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const themeColor = restaurant.menuColor || "#F4F5F9";
|
||||||
|
upsertMeta("theme-color", themeColor);
|
||||||
|
|
||||||
|
upsertLink("manifest", `/${name}/manifest.webmanifest`);
|
||||||
|
|
||||||
|
const logo = restaurant.logo?.trim();
|
||||||
|
const iconUrl =
|
||||||
|
logo && logo !== ""
|
||||||
|
? `/${name}/api/logo?url=${encodeURIComponent(logo)}&size=192`
|
||||||
|
: PWA_ICON_192;
|
||||||
|
|
||||||
|
upsertLink("icon", iconUrl);
|
||||||
|
upsertLink("apple-touch-icon", iconUrl);
|
||||||
|
}, [data, name]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useGetAbout } from "@/app/[name]/(Main)/about/hooks/useAboutData";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RestaurantNotFoundGuard({ children }: Props) {
|
||||||
|
const { name } = useParams<{ name: string }>();
|
||||||
|
const { isError, isLoading, isFetching } = useGetAbout();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading || isFetching) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[50vh] items-center justify-center px-6 text-center">
|
||||||
|
<p className="text-disabled-text">رستوران یافت نشد</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user