From 4e48ce7214f29009c0e106d366b6fe591223d854 Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 6 Jun 2026 10:17:29 +0330 Subject: [PATCH] remove ssr --- src/app/[name]/(Dialogs)/game/2048/page.tsx | 2 - src/app/[name]/(Main)/about/layout.tsx | 39 +-- src/app/[name]/(Main)/about/page.tsx | 11 +- .../[name]/(Main)/components/MenuIndex.tsx | 250 +++++++++++++++ src/app/[name]/(Main)/page.tsx | 285 +++--------------- src/app/[name]/RestaurantLayoutClient.tsx | 24 ++ src/app/[name]/layout.tsx | 115 +------ src/app/[name]/lib/restaurantMetadata.ts | 70 +++++ src/components/RestaurantHeadManager.tsx | 61 ++++ src/components/RestaurantNotFoundGuard.tsx | 31 ++ 10 files changed, 486 insertions(+), 402 deletions(-) create mode 100644 src/app/[name]/(Main)/components/MenuIndex.tsx create mode 100644 src/app/[name]/RestaurantLayoutClient.tsx create mode 100644 src/app/[name]/lib/restaurantMetadata.ts create mode 100644 src/components/RestaurantHeadManager.tsx create mode 100644 src/components/RestaurantNotFoundGuard.tsx diff --git a/src/app/[name]/(Dialogs)/game/2048/page.tsx b/src/app/[name]/(Dialogs)/game/2048/page.tsx index 5c46a16..52ed288 100644 --- a/src/app/[name]/(Dialogs)/game/2048/page.tsx +++ b/src/app/[name]/(Dialogs)/game/2048/page.tsx @@ -3,8 +3,6 @@ import { type FC } from 'react' import { useRouter } from 'next/navigation' import { CloseCircle } from 'iconsax-react' -export const dynamic = 'force-dynamic' - const Game2048Page: FC = () => { const router = useRouter() diff --git a/src/app/[name]/(Main)/about/layout.tsx b/src/app/[name]/(Main)/about/layout.tsx index 8d34297..3c56257 100644 --- a/src/app/[name]/(Main)/about/layout.tsx +++ b/src/app/[name]/(Main)/about/layout.tsx @@ -1,36 +1,5 @@ -import React from 'react' -import { Metadata } from 'next'; -import { getAboutData } from '@/lib/api/info/getAboutData'; - -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 }): Promise { - 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 default function AboutLayout({ + children, +}: Readonly<{ children: React.ReactNode }>) { + return <>{children}; } - -export async function generateStaticParams() { - return [ - { name: 'zhivan' }, - { name: 'boote' }, - ]; -} - -async function layout({ children }: Readonly<{ children: React.ReactNode; params: Promise }>) { - return ( - <>{children} - ) -} - -export default layout \ No newline at end of file diff --git a/src/app/[name]/(Main)/about/page.tsx b/src/app/[name]/(Main)/about/page.tsx index 736a69f..d8a9c99 100644 --- a/src/app/[name]/(Main)/about/page.tsx +++ b/src/app/[name]/(Main)/about/page.tsx @@ -1,10 +1,3 @@ -import React from 'react' -import AboutPage from './AboutPage' +'use client'; -const page = () => { - return ( - - ) -} - -export default page \ No newline at end of file +export { default } from './AboutPage'; diff --git a/src/app/[name]/(Main)/components/MenuIndex.tsx b/src/app/[name]/(Main)/components/MenuIndex.tsx new file mode 100644 index 0000000..89ef41d --- /dev/null +++ b/src/app/[name]/(Main)/components/MenuIndex.tsx @@ -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(() => foodsData?.data || [], [foodsData?.data]); + const categories = useMemo(() => 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 = useRef(null); + const wrapperRef: React.RefObject = 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) => { + 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 ; + } + + return ( +
+
+ + +
+ +
+
+ + {selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''} + +
+ + +
+
+ + + + + + + {filteredReceiptItems.length === 0 ? ( +
+ {foodsData ? 'غذایی یافت نشد' : 'در حال بارگذاری...'} +
+ ) : ( + filteredReceiptItems.map((food) => ( + + + + )) + )} +
+
+ + { }} + tMenu={tMenu} + /> + + +
+ ); +}; + +export default MenuIndex; diff --git a/src/app/[name]/(Main)/page.tsx b/src/app/[name]/(Main)/page.tsx index 1505af5..dd4de83 100644 --- a/src/app/[name]/(Main)/page.tsx +++ b/src/app/[name]/(Main)/page.tsx @@ -1,253 +1,46 @@ -'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"; +import type { Metadata, Viewport } from "next"; +import { notFound } from "next/navigation"; +import { getRestaurant } from "@/app/[name]/lib/getRestaurant"; +import { + buildRestaurantMetadata, + buildRestaurantViewport, +} from "@/app/[name]/lib/restaurantMetadata"; +import MenuIndex from "./components/MenuIndex"; -const sortings = ["PopularityDescending", "RateDescending", "PriceAscending", "PriceDescending"] as const; +type PageParams = { name: string }; -const MenuIndex = () => { - const { data: foodsData, isLoading: foodsLoading } = useGetFoods(); - const { data: categoriesData, isLoading: categoriesLoading } = useGetCategories(); - const foods = useMemo(() => foodsData?.data || [], [foodsData?.data]); - const categories = useMemo(() => categoriesData?.data || [], [categoriesData?.data]); +export async function generateMetadata({ + params, +}: { + params: Promise; +}): Promise { + const { name } = await params; + return buildRestaurantMetadata(name); +} - 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 = useRef(null); - const wrapperRef: React.RefObject = useRef(null); - const [smallCategoriesVisible, setSmallCategoriesVisibility] = useState(false); - const [isInitialMount, setIsInitialMount] = useState(true); +export async function generateViewport({ + params, +}: { + params: Promise; +}): Promise { + const { name } = await params; + return buildRestaurantViewport(name); +} - useEffect(() => { - if (isInitialMount) { - setSearch(null); - // category را نگه می‌داریم تا در بک زدن حفظ شود - // setSelectedCategory(null); - setSelectedIngredients(null); - setSelectedDeliveryId(null); - setSorting(null); - setIsInitialMount(false); +export default async function Page({ + params, +}: { + params: Promise; +}) { + const { name } = await params; + + try { + await getRestaurant(name); + } catch (error) { + if (error instanceof Error && error.message === "RESTAURANT_NOT_FOUND") { + notFound(); } - }, [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) => { - 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 ; } - return ( -
-
- - -
- -
-
- - {selectedCategory === '0' ? '' : categories.find(c => c.id === selectedCategory)?.title || ''} - -
- - -
-
- - - - - - - {filteredReceiptItems.length === 0 ? ( -
- {foodsData ? 'غذایی یافت نشد' : 'در حال بارگذاری...'} -
- ) : ( - filteredReceiptItems.map((food) => ( - - - - )) - )} -
-
- - { }} - tMenu={tMenu} - /> - - -
- ); -}; - -export default MenuIndex; + return ; +} diff --git a/src/app/[name]/RestaurantLayoutClient.tsx b/src/app/[name]/RestaurantLayoutClient.tsx new file mode 100644 index 0000000..a0bfa7a --- /dev/null +++ b/src/app/[name]/RestaurantLayoutClient.tsx @@ -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 ( + <> + + + {children} + + + + + ); +} diff --git a/src/app/[name]/layout.tsx b/src/app/[name]/layout.tsx index 5f66159..e510eda 100644 --- a/src/app/[name]/layout.tsx +++ b/src/app/[name]/layout.tsx @@ -1,114 +1,9 @@ -import PreferenceWrapper from '@/components/wrapper/PreferenceWrapper' -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' +import RestaurantLayoutClient from "./RestaurantLayoutClient"; -export const dynamic = 'force-dynamic' -export const revalidate = 0 - -type LayoutParams = { name: string } - -export async function generateMetadata({ - params -}: { - params: Promise -}): Promise { - 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 -}): Promise { - try { - const { name } = await params - const restaurant = await getRestaurant(name) - - return { - themeColor: restaurant.menuColor || '#F4F5F9' - } - } catch { - return { - themeColor: '#F4F5F9' - } - } -} - -export default async function Layout({ +export default function Layout({ children, - params }: { - children: React.ReactNode - params: Promise -}): Promise { - const { name } = await params - - try { - await getRestaurant(name) - } catch (error) { - if (error instanceof Error && error.message === 'RESTAURANT_NOT_FOUND') { - notFound() - } - } - - return ( - <> - {children} - - - - ) + children: React.ReactNode; +}) { + return {children}; } diff --git a/src/app/[name]/lib/restaurantMetadata.ts b/src/app/[name]/lib/restaurantMetadata.ts new file mode 100644 index 0000000..a26e0a8 --- /dev/null +++ b/src/app/[name]/lib/restaurantMetadata.ts @@ -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 { + 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 { + try { + const restaurant = await getRestaurant(name); + return { + themeColor: restaurant.menuColor || "#F4F5F9", + }; + } catch { + return { + themeColor: "#F4F5F9", + }; + } +} diff --git a/src/components/RestaurantHeadManager.tsx b/src/components/RestaurantHeadManager.tsx new file mode 100644 index 0000000..df57b8d --- /dev/null +++ b/src/components/RestaurantHeadManager.tsx @@ -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; +} diff --git a/src/components/RestaurantNotFoundGuard.tsx b/src/components/RestaurantNotFoundGuard.tsx new file mode 100644 index 0000000..cca9021 --- /dev/null +++ b/src/components/RestaurantNotFoundGuard.tsx @@ -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 ( +
+

رستوران یافت نشد

+
+ ); + } + + return <>{children}; +}