feat: add support for menu sections per category, use array instead of relationship to associate menu items with menu categories to allow sorting

main
RaviAnand Mohabir 3 weeks ago
parent 1c16cedf85
commit 4d831ae89c

@ -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<FindOneOptions<"menu">, "slug" | "depth"> = {}) => {
const payload = await getPayload();
return await payload.findGlobal({ slug: "menu", depth: 1, ...opts });
};
export const getMenuCategories = async (
opts: Omit<Options<"menu-category">, "collection" | "pagination"> = {},
opts: Omit<FindOneOptions<"menu">, "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<Options<"menu-item">, "collection" | "depth">) => {
export const getMenuCategory = async (
id: string,
opts: Omit<FindByIDOptions<"menu-category">, "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<FindByIDOptions<"menu-category">, "collection" | "depth" | "id">,
) => {
const mc = await getMenuCategory(categoryId, { depth: 2, ...opts });
return mc.sections ?? [];
};

@ -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 (
<Tabs.Content {...props}>
<Stack>
{menuItems.docs.map((mi) => (
<HStack key={mi.id} alignItems="start">
<Stack marginRight="auto">
<HStack alignItems="start">
<Text>{mi.name}</Text>
<HStack flexWrap="wrap">
{mi.tags?.map((tag) => (
<MenuItemTag key={(tag as MenuItemTagT).id} tag={tag as MenuItemTagT} />
))}
</HStack>
{mi.image && <MenuItemImage image={mi.image as Media} />}
</HStack>
{mi.description && (
<RichText content={mi.description} className={css({ color: "fg.muted" })} />
)}
</Stack>
<Stack css={{ sm: { justifyContent: "end", flexDir: "row" } }}>
{mi.variants.map((v) => (
<Stack key={v.id} align="end">
<Text>{formatToCHF(v.price!)}</Text>
<Text color="fg.muted">{v.title}</Text>
</Stack>
))}
</Stack>
</HStack>
))}
</Stack>
</Tabs.Content>
);
}

@ -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 (
<Tabs.Content {...props}>
<Stack>
{sections.map((section) => (
<>
{section.name && (
<Heading as="h3" size="xl">
{section.name}
</Heading>
)}
{(section.items as { item: MenuItem }[])?.map(({ item: mi }, idx) => (
<>
<HStack key={mi.id} alignItems="start">
<Stack marginRight="auto">
<HStack alignItems="start">
<Text>{mi.name}</Text>
<HStack flexWrap="wrap">
{mi.tags?.map((tag) => (
<MenuItemTag key={(tag as MenuItemTagT).id} tag={tag as MenuItemTagT} />
))}
</HStack>
{mi.image && <MenuItemImage image={mi.image as Media} />}
</HStack>
{mi.description && (
<RichText content={mi.description} className={css({ color: "fg.muted" })} />
)}
</Stack>
<Stack css={{ sm: { justifyContent: "end", flexDir: "row" } }}>
{mi.variants?.map((v) => (
<Stack key={v.id} align="end">
<Text>{formatToCHF(v.price!)}</Text>
<Text color="fg.muted">{v.title}</Text>
</Stack>
))}
</Stack>
</HStack>
{idx !== section.items.length - 1 && <Divider orientation="horizontal" />}
</>
))}
</>
))}
</Stack>
</Tabs.Content>
);
}

@ -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")}
</Heading>
<Box maxW="3xl" w="100%">
<Tabs.Root defaultValue={menuCategories.docs[0].id} orientation="horizontal" w="100%">
<Tabs.Root
defaultValue={(menuCategories[0] as MenuCategory).id}
orientation="horizontal"
w="100%"
>
<Tabs.List>
{menuCategories.docs.map((mc) => (
<Tabs.Trigger key={mc.id} value={mc.id}>
{mc.name}
{menuCategories.map((mc) => (
<Tabs.Trigger key={(mc as MenuCategory).id} value={(mc as MenuCategory).id}>
{(mc as MenuCategory).name}
</Tabs.Trigger>
))}
<Tabs.Indicator />
</Tabs.List>
{menuCategories.docs.map((mc) => (
<CategoryTabContent key={mc.id} value={mc.id} locale={locale}></CategoryTabContent>
{menuCategories.map((mc) => (
<MenuCategoryTabContent
key={(mc as MenuCategory).id}
value={(mc as MenuCategory).id}
locale={locale}
></MenuCategoryTabContent>
))}
</Tabs.Root>
</Box>

@ -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,
},
],
};

@ -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,
},
],
};

@ -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,
},
],
},
],
};

@ -27,11 +27,6 @@ export const MenuItem: CollectionConfig = {
type: "relationship",
relationTo: "media",
},
{
name: "category",
type: "relationship",
relationTo: "menu-category",
},
{
name: "tags",
type: "relationship",

@ -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,
},
],
};

@ -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",

@ -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".

@ -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: {

Loading…
Cancel
Save