From 4d831ae89c161880fb921e6e5cc05cad4af753d2 Mon Sep 17 00:00:00 2001 From: RaviAnand Mohabir Date: Wed, 28 Aug 2024 08:59:05 +0200 Subject: [PATCH] feat: :sparkles: add support for menu sections per category, use array instead of relationship to associate menu items with menu categories to allow sorting --- src/api/menu.ts | 28 ++- .../[locale]/menu/category-tab-content.tsx | 57 ------- .../menu/menu-category-tab-content.tsx | 68 ++++++++ src/app/(frontend)/[locale]/menu/page.tsx | 23 ++- src/blocks/MenuItem.ts | 13 ++ src/blocks/MenuSection.ts | 18 ++ src/collections/MenuCategory.ts | 31 ++++ src/collections/MenuItem.ts | 5 - src/collections/MenuSection.ts | 26 +++ src/globals/Menu.ts | 11 ++ src/payload-types.ts | 161 ++++++++++++------ src/payload.config.ts | 11 +- 12 files changed, 319 insertions(+), 133 deletions(-) delete mode 100644 src/app/(frontend)/[locale]/menu/category-tab-content.tsx create mode 100644 src/app/(frontend)/[locale]/menu/menu-category-tab-content.tsx create mode 100644 src/blocks/MenuItem.ts create mode 100644 src/blocks/MenuSection.ts create mode 100644 src/collections/MenuSection.ts diff --git a/src/api/menu.ts b/src/api/menu.ts index c10c2aa..268444b 100644 --- a/src/api/menu.ts +++ b/src/api/menu.ts @@ -1,14 +1,32 @@ +import type { Options as FindByIDOptions } from "node_modules/payload/dist/collections/operations/local/findByID"; +import type { Options as FindOneOptions } from "node_modules/payload/dist/globals/operations/local/findOne"; import type { Options } from "node_modules/payload/dist/collections/operations/local/find"; import { getPayload } from "@/utils/payload"; +export const getMenu = async (opts: Omit, "slug" | "depth"> = {}) => { + const payload = await getPayload(); + return await payload.findGlobal({ slug: "menu", depth: 1, ...opts }); +}; + export const getMenuCategories = async ( - opts: Omit, "collection" | "pagination"> = {}, + opts: Omit, "slug" | "depth"> = {}, ) => { - const payload = await getPayload(); - return payload.find({ collection: "menu-category", pagination: false, ...opts }); + const menu = await getMenu(opts); + return menu.categories; }; -export const getMenuItems = async (opts: Omit, "collection" | "depth">) => { +export const getMenuCategory = async ( + id: string, + opts: Omit, "collection" | "id">, +) => { const payload = await getPayload(); - return payload.find({ collection: "menu-item", depth: 1, ...opts }); + return payload.findByID({ id, collection: "menu-category", ...opts }); +}; + +export const getMenuSections = async ( + categoryId: string, + opts: Omit, "collection" | "depth" | "id">, +) => { + const mc = await getMenuCategory(categoryId, { depth: 2, ...opts }); + return mc.sections ?? []; }; diff --git a/src/app/(frontend)/[locale]/menu/category-tab-content.tsx b/src/app/(frontend)/[locale]/menu/category-tab-content.tsx deleted file mode 100644 index d174df7..0000000 --- a/src/app/(frontend)/[locale]/menu/category-tab-content.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { HStack, Stack } from "@styled-system/jsx"; -import type { Media, MenuItemTag as MenuItemTagT } from "@/payload-types"; - -import { Locale } from "@/i18n/settings"; -import MenuItemImage from "@/app/(frontend)/[locale]/menu/menu-item-image"; -import MenuItemTag from "./menu-item-tag"; -import RichText from "@/components/rich-text"; -import { TabContentBaseProps } from "@ark-ui/react"; -import { Tabs } from "@/components/ui/tabs"; -import { Text } from "@/components/ui/text"; -import { css } from "@styled-system/css"; -import { formatToCHF } from "@/utils/formatters"; -import { getMenuItems } from "@/api"; - -export default async function CategoryTabContent({ - locale, - ...props -}: { locale: Locale } & TabContentBaseProps) { - const menuItems = await getMenuItems({ - locale, - where: { category: { equals: props.value } }, - pagination: false, - }); - - return ( - - - {menuItems.docs.map((mi) => ( - - - - {mi.name} - - {mi.tags?.map((tag) => ( - - ))} - - {mi.image && } - - {mi.description && ( - - )} - - - {mi.variants.map((v) => ( - - {formatToCHF(v.price!)} - {v.title} - - ))} - - - ))} - - - ); -} diff --git a/src/app/(frontend)/[locale]/menu/menu-category-tab-content.tsx b/src/app/(frontend)/[locale]/menu/menu-category-tab-content.tsx new file mode 100644 index 0000000..d16bc40 --- /dev/null +++ b/src/app/(frontend)/[locale]/menu/menu-category-tab-content.tsx @@ -0,0 +1,68 @@ +import { Divider, HStack, Stack } from "@styled-system/jsx"; +import type { Media, MenuItem, MenuItemTag as MenuItemTagT } from "@/payload-types"; + +import { Heading } from "@/components/ui/heading"; +import { Locale } from "@/i18n/settings"; +import MenuItemImage from "@/app/(frontend)/[locale]/menu/menu-item-image"; +import MenuItemTag from "./menu-item-tag"; +import RichText from "@/components/rich-text"; +import { TabContentBaseProps } from "@ark-ui/react"; +import { Tabs } from "@/components/ui/tabs"; +import { Text } from "@/components/ui/text"; +import { css } from "@styled-system/css"; +import { formatToCHF } from "@/utils/formatters"; +import { getMenuSections } from "@/api"; + +export default async function MenuCategoryTabContent({ + locale, + ...props +}: { locale: Locale } & TabContentBaseProps) { + const sections = await getMenuSections(props.value, { + locale, + }); + + return ( + + + {sections.map((section) => ( + <> + {section.name && ( + + {section.name} + + )} + {(section.items as { item: MenuItem }[])?.map(({ item: mi }, idx) => ( + <> + + + + {mi.name} + + {mi.tags?.map((tag) => ( + + ))} + + {mi.image && } + + {mi.description && ( + + )} + + + {mi.variants?.map((v) => ( + + {formatToCHF(v.price!)} + {v.title} + + ))} + + + {idx !== section.items.length - 1 && } + + ))} + + ))} + + + ); +} diff --git a/src/app/(frontend)/[locale]/menu/page.tsx b/src/app/(frontend)/[locale]/menu/page.tsx index 92904ff..7904401 100644 --- a/src/app/(frontend)/[locale]/menu/page.tsx +++ b/src/app/(frontend)/[locale]/menu/page.tsx @@ -1,7 +1,8 @@ import { Box, Stack } from "@styled-system/jsx"; -import CategoryTabContent from "./category-tab-content"; import { Heading } from "@/components/ui/heading"; +import { MenuCategory } from "@/payload-types"; +import MenuCategoryTabContent from "./menu-category-tab-content"; import { Params } from "../shared"; import { Tabs } from "@/components/ui/tabs"; import { getI18n } from "@/i18n/server"; @@ -17,17 +18,25 @@ export default async function Menu({ params: { locale } }: { params: Params }) { {t("general.menu")} - + - {menuCategories.docs.map((mc) => ( - - {mc.name} + {menuCategories.map((mc) => ( + + {(mc as MenuCategory).name} ))} - {menuCategories.docs.map((mc) => ( - + {menuCategories.map((mc) => ( + ))} diff --git a/src/blocks/MenuItem.ts b/src/blocks/MenuItem.ts new file mode 100644 index 0000000..e70a663 --- /dev/null +++ b/src/blocks/MenuItem.ts @@ -0,0 +1,13 @@ +import { Block } from "payload"; + +export const MenuItemBlock: Block = { + slug: "menu-item", + fields: [ + { + name: "item", + type: "relationship", + relationTo: "menu-item", + required: true, + }, + ], +}; diff --git a/src/blocks/MenuSection.ts b/src/blocks/MenuSection.ts new file mode 100644 index 0000000..27911be --- /dev/null +++ b/src/blocks/MenuSection.ts @@ -0,0 +1,18 @@ +import { Block } from "payload"; + +export const MenuSectionBlock: Block = { + slug: "menu-section", + fields: [ + { + name: "name", + type: "text", + localized: true, + required: true, + }, + { + name: "description", + type: "richText", + localized: true, + }, + ], +}; diff --git a/src/collections/MenuCategory.ts b/src/collections/MenuCategory.ts index 1975169..ab49c0c 100644 --- a/src/collections/MenuCategory.ts +++ b/src/collections/MenuCategory.ts @@ -1,5 +1,7 @@ import type { CollectionConfig } from "payload"; import { Menu } from "../groups/Menu"; +import { MenuItemBlock } from "@/blocks/MenuItem"; +import { MenuSectionBlock } from "@/blocks/MenuSection"; export const MenuCategory: CollectionConfig = { slug: "menu-category", @@ -16,5 +18,34 @@ export const MenuCategory: CollectionConfig = { type: "text", localized: true, }, + { + name: "sections", + type: "array", + fields: [ + { + name: "name", + type: "text", + localized: true, + }, + { + name: "description", + type: "richText", + localized: true, + }, + { + name: "items", + type: "array", + fields: [ + { + name: "item", + type: "relationship", + relationTo: "menu-item", + required: true, + }, + ], + required: true, + }, + ], + }, ], }; diff --git a/src/collections/MenuItem.ts b/src/collections/MenuItem.ts index 8ed0abc..7a7b9d9 100644 --- a/src/collections/MenuItem.ts +++ b/src/collections/MenuItem.ts @@ -27,11 +27,6 @@ export const MenuItem: CollectionConfig = { type: "relationship", relationTo: "media", }, - { - name: "category", - type: "relationship", - relationTo: "menu-category", - }, { name: "tags", type: "relationship", diff --git a/src/collections/MenuSection.ts b/src/collections/MenuSection.ts new file mode 100644 index 0000000..5e78ab9 --- /dev/null +++ b/src/collections/MenuSection.ts @@ -0,0 +1,26 @@ +import type { CollectionConfig } from "payload"; +import { Menu } from "../groups/Menu"; + +export const MenuSection: CollectionConfig = { + slug: "menu-section", + access: { + read: () => true, + }, + admin: { + useAsTitle: "name", + group: Menu, + }, + fields: [ + { + name: "name", + type: "text", + required: true, + localized: true, + }, + { + name: "description", + type: "richText", + localized: true, + }, + ], +}; diff --git a/src/globals/Menu.ts b/src/globals/Menu.ts index 796bcea..1031b6c 100644 --- a/src/globals/Menu.ts +++ b/src/globals/Menu.ts @@ -1,16 +1,27 @@ import type { GlobalConfig } from "payload"; +import { Menu as MenuGroup } from "@/groups/Menu"; export const Menu: GlobalConfig = { slug: "menu", access: { read: () => true, }, + admin: { + group: MenuGroup, + }, fields: [ { name: "file", type: "relationship", relationTo: "media", }, + { + name: "categories", + type: "relationship", + relationTo: "menu-category", + hasMany: true, + required: true, + }, { name: "specials", type: "relationship", diff --git a/src/payload-types.ts b/src/payload-types.ts index ad836ef..07729ca 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -14,13 +14,14 @@ export interface Config { users: User; media: Media; 'opening-time': OpeningTime; + announcement: Announcement; + vacation: Vacation; + holiday: Holiday; 'menu-item': MenuItem; 'menu-category': MenuCategory; 'menu-item-tag': MenuItemTag; + 'menu-section': MenuSection; 'food-declaration': FoodDeclaration; - announcement: Announcement; - vacation: Vacation; - holiday: Holiday; 'payload-preferences': PayloadPreference; 'payload-migrations': PayloadMigration; }; @@ -30,9 +31,9 @@ export interface Config { globals: { home: Home; gallery: Gallery; + menu: Menu; about: About; contact: Contact; - menu: Menu; settings: Setting; }; locale: 'de' | 'fr' | 'it' | 'en'; @@ -109,11 +110,11 @@ export interface OpeningTime { } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "menu-item". + * via the `definition` "announcement". */ -export interface MenuItem { +export interface Announcement { id: string; - name: string; + title: string; description?: { root: { type: string; @@ -129,32 +130,40 @@ export interface MenuItem { }; [k: string]: unknown; } | null; - image?: (string | null) | Media; - category?: (string | null) | MenuCategory; - tags?: (string | MenuItemTag)[] | null; - variants: { - title?: string | null; - price: number; - id?: string | null; - }[]; + from: string; + to?: string | null; updatedAt: string; createdAt: string; } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "menu-category". + * via the `definition` "vacation". */ -export interface MenuCategory { +export interface Vacation { id: string; - name?: string | null; + title: string; + from: string; + to?: string | null; updatedAt: string; createdAt: string; } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "menu-item-tag". + * via the `definition` "holiday". */ -export interface MenuItemTag { +export interface Holiday { + id: string; + title: string; + from: string; + to?: string | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "menu-item". + */ +export interface MenuItem { id: string; name: string; description?: { @@ -172,16 +181,23 @@ export interface MenuItemTag { }; [k: string]: unknown; } | null; + image?: (string | null) | Media; + tags?: (string | MenuItemTag)[] | null; + variants: { + title?: string | null; + price: number; + id?: string | null; + }[]; updatedAt: string; createdAt: string; } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "food-declaration". + * via the `definition` "menu-item-tag". */ -export interface FoodDeclaration { +export interface MenuItemTag { id: string; - title: string; + name: string; description?: { root: { type: string; @@ -202,11 +218,46 @@ export interface FoodDeclaration { } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "announcement". + * via the `definition` "menu-category". */ -export interface Announcement { +export interface MenuCategory { id: string; - title: string; + name?: string | null; + sections?: + | { + name?: string | null; + description?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + items: { + item: string | MenuItem; + id?: string | null; + }[]; + id?: string | null; + }[] + | null; + updatedAt: string; + createdAt: string; +} +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "menu-section". + */ +export interface MenuSection { + id: string; + name: string; description?: { root: { type: string; @@ -222,32 +273,31 @@ export interface Announcement { }; [k: string]: unknown; } | null; - from: string; - to?: string | null; - updatedAt: string; - createdAt: string; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "vacation". - */ -export interface Vacation { - id: string; - title: string; - from: string; - to?: string | null; updatedAt: string; createdAt: string; } /** * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "holiday". + * via the `definition` "food-declaration". */ -export interface Holiday { +export interface FoodDeclaration { id: string; title: string; - from: string; - to?: string | null; + description?: { + root: { + type: string; + children: { + type: string; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; updatedAt: string; createdAt: string; } @@ -324,6 +374,18 @@ export interface Gallery { updatedAt?: string | null; createdAt?: string | null; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "menu". + */ +export interface Menu { + id: string; + file?: (string | null) | Media; + categories: (string | MenuCategory)[]; + specials?: (string | MenuItem)[] | null; + updatedAt?: string | null; + createdAt?: string | null; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "about". @@ -379,17 +441,6 @@ export interface Contact { updatedAt?: string | null; createdAt?: string | null; } -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "menu". - */ -export interface Menu { - id: string; - file?: (string | null) | Media; - specials?: (string | MenuItem)[] | null; - updatedAt?: string | null; - createdAt?: string | null; -} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "settings". diff --git a/src/payload.config.ts b/src/payload.config.ts index b006574..2d8133b 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -14,6 +14,7 @@ import { Menu } from "@/globals/Menu"; import { MenuCategory } from "@/collections/MenuCategory"; import { MenuItem } from "@/collections/MenuItem"; import { MenuItemTag } from "@/collections/MenuItemTag"; +import { MenuSection } from "@/collections/MenuSection"; import { OpeningTime } from "@/collections/OpeningTime"; import { Settings } from "@/globals/Settings"; import { Users } from "@/collections/Users"; @@ -43,15 +44,17 @@ export default buildConfig({ Users, Media, OpeningTime, + Announcement, + Vacation, + Holiday, + /* Menu */ MenuItem, MenuCategory, MenuItemTag, + MenuSection, FoodDeclaration, - Announcement, - Vacation, - Holiday, ], - globals: [Home, Gallery, About, Contact, Menu, Settings], + globals: [Home, Gallery, Menu, About, Contact, Settings], editor: lexicalEditor(), secret: process.env.PAYLOAD_SECRET || "", typescript: {