feat: ✨ implement menu and contact pages with opening times, support for iframe embed and menu item images, variants and descriptions
parent
d436902d25
commit
ea6a15e99a
@ -0,0 +1,12 @@
|
||||
import { Options } from "node_modules/payload/dist/collections/operations/local/find";
|
||||
import { getPayload } from "@/utils/payload";
|
||||
|
||||
export const getMenuCategories = async () => {
|
||||
const payload = await getPayload();
|
||||
return payload.find({ collection: "menu-category", pagination: false });
|
||||
};
|
||||
|
||||
export const getMenuItems = async (opts: Omit<Options<"menu-item">, "collection">) => {
|
||||
const payload = await getPayload();
|
||||
return payload.find({ collection: "menu-item", ...opts });
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
import { getPayload } from "@/utils/payload";
|
||||
|
||||
export const getOpeningTimes = async () => {
|
||||
const payload = await getPayload();
|
||||
return await payload.find({ collection: "opening-time", sort: "from", pagination: false });
|
||||
};
|
@ -1 +1,78 @@
|
||||
export default async function Contact() {}
|
||||
import { Box, HStack, Stack } from "@styled-system/jsx";
|
||||
import { getAbout, getContact } from "@/api";
|
||||
|
||||
import { Heading } from "@/components/ui/heading";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { getI18n } from "@/i18n/server";
|
||||
import { getOpeningTimes } from "@/api/openingTimes";
|
||||
import { styled } from "@styled-system/jsx";
|
||||
|
||||
export default async function Contact() {
|
||||
const t = await getI18n();
|
||||
const about = await getAbout();
|
||||
const contact = await getContact();
|
||||
const openingTimes = await getOpeningTimes();
|
||||
|
||||
return (
|
||||
<Stack p={6} gap={10}>
|
||||
<Heading as="h1" size="3xl">
|
||||
{t("general.contact")}
|
||||
</Heading>
|
||||
|
||||
<Stack>
|
||||
<Heading as="h3" size="xl">
|
||||
{t("general.openingTimes")}
|
||||
</Heading>
|
||||
{openingTimes.docs.map((ot) => (
|
||||
<HStack key={ot.id}>
|
||||
<Text w={250}>
|
||||
{t(`days.${ot.from}`)}
|
||||
{ot.to ? `- ${t(`days.${ot.to}`)}` : ""}
|
||||
</Text>
|
||||
<Text>
|
||||
{new Date(ot.timeOpen).toLocaleTimeString("de-CH", { timeStyle: "short" })} -{" "}
|
||||
{new Date(ot.timeClose).toLocaleTimeString("de-CH", { timeStyle: "short" })}
|
||||
</Text>
|
||||
</HStack>
|
||||
))}
|
||||
</Stack>
|
||||
|
||||
<Stack>
|
||||
<Heading as="h3" size="xl">
|
||||
{t("general.address")}
|
||||
</Heading>
|
||||
<HStack gap={20}>
|
||||
{contact.address.embeddedMaps && (
|
||||
<styled.div
|
||||
dangerouslySetInnerHTML={{ __html: contact.address.embeddedMaps }}
|
||||
width={600}
|
||||
/>
|
||||
)}
|
||||
<Stack>
|
||||
<Box>
|
||||
<Text fontWeight="bold">{about.name}</Text>
|
||||
<Text>
|
||||
{contact.address.street} {contact.address.number}
|
||||
</Text>
|
||||
<Text>
|
||||
{contact.address.zip} {contact.address.area}
|
||||
</Text>
|
||||
</Box>
|
||||
{contact.phone && (
|
||||
<HStack>
|
||||
<Text fontWeight="bold">{t("general.phoneNumber")}</Text>
|
||||
<Text>{contact.phone}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
{contact.email && (
|
||||
<HStack>
|
||||
<Text fontWeight="bold">{t("general.email")}</Text>
|
||||
<Text>{contact.email}</Text>
|
||||
</HStack>
|
||||
)}
|
||||
</Stack>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { Box, HStack, Stack } from "@styled-system/jsx";
|
||||
|
||||
import { HoverCard } from "@/components/ui/hover-card";
|
||||
import { IconButton } from "@/components/ui/icon-button";
|
||||
import Image from "next/image";
|
||||
import { Image as ImageIcon } from "lucide-react";
|
||||
import { Media } from "@/payload-types";
|
||||
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";
|
||||
import { locales } from "@/i18n/settings";
|
||||
|
||||
export default async function CategoryTabContent({
|
||||
locale,
|
||||
...props
|
||||
}: { locale: (typeof locales)[number] } & 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>
|
||||
<Text>{mi.name}</Text>
|
||||
{mi.image && (
|
||||
<HoverCard.Root>
|
||||
<HoverCard.Trigger asChild>
|
||||
<IconButton variant="ghost">
|
||||
<ImageIcon />
|
||||
</IconButton>
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Positioner>
|
||||
<HoverCard.Content>
|
||||
<HoverCard.Arrow>
|
||||
<HoverCard.ArrowTip />
|
||||
</HoverCard.Arrow>
|
||||
<Box w={250} h={150} position="relative">
|
||||
<Image
|
||||
src={(mi.image as Media).url!}
|
||||
alt={(mi.image as Media).alt ?? ""}
|
||||
className={css({ objectFit: "cover" })}
|
||||
fill
|
||||
/>
|
||||
</Box>
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Positioner>
|
||||
</HoverCard.Root>
|
||||
)}
|
||||
</HStack>
|
||||
{mi.description && (
|
||||
<RichText content={mi.description} className={css({ color: "fg.muted" })} />
|
||||
)}
|
||||
</Stack>
|
||||
<HStack justify="end">
|
||||
{mi.variants.map((v) => (
|
||||
<Stack key={v.id} align="end">
|
||||
<Text>{formatToCHF(v.price!)}</Text>
|
||||
<Text color="fg.muted">{v.title}</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</HStack>
|
||||
</HStack>
|
||||
))}
|
||||
</Stack>
|
||||
</Tabs.Content>
|
||||
);
|
||||
}
|
@ -1 +1,36 @@
|
||||
export default async function Menu() {}
|
||||
import { Box, Stack } from "@styled-system/jsx";
|
||||
|
||||
import CategoryTabContent from "./category-tab-content";
|
||||
import { Heading } from "@/components/ui/heading";
|
||||
import { Params } from "../shared";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import { getI18n } from "@/i18n/server";
|
||||
import { getMenuCategories } from "@/api";
|
||||
|
||||
export default async function Menu({ params: { locale } }: { params: Params }) {
|
||||
const t = await getI18n();
|
||||
const menuCategories = await getMenuCategories();
|
||||
|
||||
return (
|
||||
<Stack p={6} align="center">
|
||||
<Heading as="h1" size="3xl">
|
||||
{t("general.menu")}
|
||||
</Heading>
|
||||
<Box maxW="3xl" w="100%">
|
||||
<Tabs.Root defaultValue={menuCategories.docs[0].id} orientation="horizontal" w="100%">
|
||||
<Tabs.List>
|
||||
{menuCategories.docs.map((mc) => (
|
||||
<Tabs.Trigger key={mc.id} value={mc.id}>
|
||||
{mc.name}
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
<Tabs.Indicator />
|
||||
</Tabs.List>
|
||||
{menuCategories.docs.map((mc) => (
|
||||
<CategoryTabContent key={mc.id} value={mc.id} locale={locale}></CategoryTabContent>
|
||||
))}
|
||||
</Tabs.Root>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { locales } from "@/i18n/settings";
|
||||
|
||||
export type Params = { locale: (typeof locales)[number] };
|
@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useChangeLocale, useCurrentLocale, useI18n } from "@/i18n/client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Menu } from "@/components/ui/menu";
|
||||
import { locales } from "@/i18n/settings";
|
||||
|
||||
export default function LanguagePicker(props: Menu.RootProps) {
|
||||
const changeLocale = useChangeLocale();
|
||||
const locale = useCurrentLocale();
|
||||
const t = useI18n();
|
||||
|
||||
return (
|
||||
<Menu.Root {...props} positioning={{ offset: { crossAxis: -35 } }}>
|
||||
<Menu.Trigger asChild>
|
||||
<Button variant="outline" size={props.size}>
|
||||
{t(`general.${locale}`)}
|
||||
</Button>
|
||||
</Menu.Trigger>
|
||||
<Menu.Positioner>
|
||||
<Menu.Content right={0}>
|
||||
<Menu.ItemGroup>
|
||||
{locales.map((locale) => (
|
||||
<Menu.Item asChild key={locale} value={locale} onClick={() => changeLocale(locale)}>
|
||||
<Button>{t(`general.${locale}`)}</Button>
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.ItemGroup>
|
||||
</Menu.Content>
|
||||
</Menu.Positioner>
|
||||
</Menu.Root>
|
||||
);
|
||||
}
|
@ -0,0 +1 @@
|
||||
export { Heading, type HeadingProps } from './styled/heading'
|
@ -0,0 +1 @@
|
||||
export * as HoverCard from './styled/hover-card'
|
@ -0,0 +1 @@
|
||||
export * as Menu from './styled/menu'
|
@ -0,0 +1,10 @@
|
||||
import { styled } from 'styled-system/jsx'
|
||||
import { type TextVariantProps, text } from 'styled-system/recipes'
|
||||
import type { ComponentProps, StyledComponent } from 'styled-system/types'
|
||||
|
||||
type TextProps = TextVariantProps & { as?: React.ElementType }
|
||||
|
||||
export type HeadingProps = ComponentProps<typeof Heading>
|
||||
export const Heading = styled('h2', text, {
|
||||
defaultProps: { variant: 'heading' },
|
||||
}) as StyledComponent<'h2', TextProps>
|
@ -0,0 +1,45 @@
|
||||
'use client'
|
||||
import type { Assign } from '@ark-ui/react'
|
||||
import { HoverCard } from '@ark-ui/react/hover-card'
|
||||
import { type HoverCardVariantProps, hoverCard } from 'styled-system/recipes'
|
||||
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
|
||||
import { createStyleContext } from './utils/create-style-context'
|
||||
|
||||
const { withRootProvider, withContext } = createStyleContext(hoverCard)
|
||||
|
||||
export type RootProviderProps = ComponentProps<typeof RootProvider>
|
||||
export const RootProvider = withRootProvider<
|
||||
Assign<HoverCard.RootProviderProps, HoverCardVariantProps>
|
||||
>(HoverCard.RootProvider)
|
||||
|
||||
export type RootProps = ComponentProps<typeof Root>
|
||||
export const Root = withRootProvider<Assign<HoverCard.RootProps, HoverCardVariantProps>>(
|
||||
HoverCard.Root,
|
||||
)
|
||||
|
||||
export const Arrow = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, HoverCard.ArrowBaseProps>
|
||||
>(HoverCard.Arrow, 'arrow')
|
||||
|
||||
export const ArrowTip = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, HoverCard.ArrowTipBaseProps>
|
||||
>(HoverCard.ArrowTip, 'arrowTip')
|
||||
|
||||
export const Content = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, HoverCard.ContentBaseProps>
|
||||
>(HoverCard.Content, 'content')
|
||||
|
||||
export const Positioner = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, HoverCard.PositionerBaseProps>
|
||||
>(HoverCard.Positioner, 'positioner')
|
||||
|
||||
export const Trigger = withContext<
|
||||
HTMLButtonElement,
|
||||
Assign<HTMLStyledProps<'button'>, HoverCard.TriggerBaseProps>
|
||||
>(HoverCard.Trigger, 'trigger')
|
||||
|
||||
export { HoverCardContext as Context } from '@ark-ui/react/hover-card'
|
@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
import type { Assign } from '@ark-ui/react'
|
||||
import { Menu } from '@ark-ui/react/menu'
|
||||
import { type MenuVariantProps, menu } from 'styled-system/recipes'
|
||||
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
|
||||
import { createStyleContext } from './utils/create-style-context'
|
||||
|
||||
const { withRootProvider, withContext } = createStyleContext(menu)
|
||||
|
||||
export type RootProviderProps = ComponentProps<typeof RootProvider>
|
||||
export const RootProvider = withRootProvider<Assign<Menu.RootProviderProps, MenuVariantProps>>(
|
||||
Menu.RootProvider,
|
||||
)
|
||||
|
||||
export type RootProps = ComponentProps<typeof Root>
|
||||
export const Root = withRootProvider<Assign<Menu.RootProps, MenuVariantProps>>(Menu.Root)
|
||||
|
||||
export const Arrow = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ArrowBaseProps>
|
||||
>(Menu.Arrow, 'arrow')
|
||||
|
||||
export const ArrowTip = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ArrowTipBaseProps>
|
||||
>(Menu.ArrowTip, 'arrowTip')
|
||||
|
||||
export const CheckboxItem = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.CheckboxItemBaseProps>
|
||||
>(Menu.CheckboxItem, 'item')
|
||||
|
||||
export const Content = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ContentBaseProps>
|
||||
>(Menu.Content, 'content')
|
||||
|
||||
export const ContextTrigger = withContext<
|
||||
HTMLButtonElement,
|
||||
Assign<HTMLStyledProps<'button'>, Menu.ContextTriggerBaseProps>
|
||||
>(Menu.ContextTrigger, 'contextTrigger')
|
||||
|
||||
export const Indicator = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.IndicatorBaseProps>
|
||||
>(Menu.Indicator, 'indicator')
|
||||
|
||||
export const ItemGroupLabel = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ItemGroupLabelBaseProps>
|
||||
>(Menu.ItemGroupLabel, 'itemGroupLabel')
|
||||
|
||||
export const ItemGroup = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ItemGroupBaseProps>
|
||||
>(Menu.ItemGroup, 'itemGroup')
|
||||
|
||||
export const ItemIndicator = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ItemIndicatorBaseProps>
|
||||
>(Menu.ItemIndicator, 'itemIndicator')
|
||||
|
||||
export const Item = withContext<HTMLDivElement, Assign<HTMLStyledProps<'div'>, Menu.ItemBaseProps>>(
|
||||
Menu.Item,
|
||||
'item',
|
||||
)
|
||||
|
||||
export const ItemText = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.ItemTextBaseProps>
|
||||
>(Menu.ItemText, 'itemText')
|
||||
|
||||
export const Positioner = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.PositionerBaseProps>
|
||||
>(Menu.Positioner, 'positioner')
|
||||
|
||||
export const RadioItemGroup = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.RadioItemGroupBaseProps>
|
||||
>(Menu.RadioItemGroup, 'itemGroup')
|
||||
|
||||
export const RadioItem = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.RadioItemBaseProps>
|
||||
>(Menu.RadioItem, 'item')
|
||||
|
||||
export const Separator = withContext<
|
||||
HTMLHRElement,
|
||||
Assign<HTMLStyledProps<'hr'>, Menu.SeparatorBaseProps>
|
||||
>(Menu.Separator, 'separator')
|
||||
|
||||
export const TriggerItem = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Menu.TriggerItemBaseProps>
|
||||
>(Menu.TriggerItem, 'triggerItem')
|
||||
|
||||
export const Trigger = withContext<
|
||||
HTMLButtonElement,
|
||||
Assign<HTMLStyledProps<'button'>, Menu.TriggerBaseProps>
|
||||
>(Menu.Trigger, 'trigger')
|
||||
|
||||
export { MenuContext as Context } from '@ark-ui/react/menu'
|
@ -0,0 +1,42 @@
|
||||
'use client'
|
||||
import type { Assign } from '@ark-ui/react'
|
||||
import { Tabs } from '@ark-ui/react/tabs'
|
||||
import { type TabsVariantProps, tabs } from 'styled-system/recipes'
|
||||
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
|
||||
import { createStyleContext } from './utils/create-style-context'
|
||||
|
||||
const { withProvider, withContext } = createStyleContext(tabs)
|
||||
|
||||
export type RootProviderProps = ComponentProps<typeof RootProvider>
|
||||
export const RootProvider = withProvider<
|
||||
HTMLDivElement,
|
||||
Assign<Assign<HTMLStyledProps<'div'>, Tabs.RootProviderBaseProps>, TabsVariantProps>
|
||||
>(Tabs.RootProvider, 'root')
|
||||
|
||||
export type RootProps = ComponentProps<typeof Root>
|
||||
export const Root = withProvider<
|
||||
HTMLDivElement,
|
||||
Assign<Assign<HTMLStyledProps<'div'>, Tabs.RootBaseProps>, TabsVariantProps>
|
||||
>(Tabs.Root, 'root')
|
||||
|
||||
export const Content = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Tabs.ContentBaseProps>
|
||||
>(Tabs.Content, 'content')
|
||||
|
||||
export const Indicator = withContext<
|
||||
HTMLDivElement,
|
||||
Assign<HTMLStyledProps<'div'>, Tabs.IndicatorBaseProps>
|
||||
>(Tabs.Indicator, 'indicator')
|
||||
|
||||
export const List = withContext<HTMLDivElement, Assign<HTMLStyledProps<'div'>, Tabs.ListBaseProps>>(
|
||||
Tabs.List,
|
||||
'list',
|
||||
)
|
||||
|
||||
export const Trigger = withContext<
|
||||
HTMLButtonElement,
|
||||
Assign<HTMLStyledProps<'button'>, Tabs.TriggerBaseProps>
|
||||
>(Tabs.Trigger, 'trigger')
|
||||
|
||||
export { TabsContext as Context } from '@ark-ui/react/tabs'
|
@ -0,0 +1,8 @@
|
||||
import { styled } from 'styled-system/jsx'
|
||||
import { type TextVariantProps, text } from 'styled-system/recipes'
|
||||
import type { ComponentProps, StyledComponent } from 'styled-system/types'
|
||||
|
||||
type ParagraphProps = TextVariantProps & { as?: React.ElementType }
|
||||
|
||||
export type TextProps = ComponentProps<typeof Text>
|
||||
export const Text = styled('p', text) as StyledComponent<'p', ParagraphProps>
|
@ -0,0 +1 @@
|
||||
export * as Tabs from './styled/tabs'
|
@ -0,0 +1 @@
|
||||
export { Text, type TextProps } from './styled/text'
|
@ -1,7 +1,25 @@
|
||||
export default {
|
||||
general: {
|
||||
home: "Startseite",
|
||||
about: "Über uns",
|
||||
menu: "Menü",
|
||||
menu: "Speisekarte",
|
||||
contact: "Kontakt",
|
||||
address: "Addresse",
|
||||
phoneNumber: "Telefonnummer",
|
||||
email: "E-Mail",
|
||||
openingTimes: "Öffnungszeiten",
|
||||
en: "Englisch",
|
||||
de: "Deutsch",
|
||||
fr: "Französisch",
|
||||
it: "Italienisch",
|
||||
},
|
||||
days: {
|
||||
"0": "Montag",
|
||||
"1": "Dienstag",
|
||||
"2": "Mittwoch",
|
||||
"3": "Donnerstag",
|
||||
"4": "Freitag",
|
||||
"5": "Samstag",
|
||||
"6": "Sonntag",
|
||||
},
|
||||
} as const;
|
||||
|
@ -0,0 +1,2 @@
|
||||
export const formatToCHF = (val: number) =>
|
||||
Intl.NumberFormat("de-CH", { style: "currency", currency: "CHF" }).format(val);
|
Loading…
Reference in New Issue