feat: implement simple landing page with image, tagline, short about text and image gallery

- Add ParkUI
- Add lucide-react
- Add RichText renderer
- Add Moderustic font
main
RaviAnand Mohabir 3 months ago
parent c3dd300769
commit 34be1c9ad7

@ -0,0 +1,4 @@
{
"typescript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.importModuleSpecifier": "non-relative"
}

@ -26,6 +26,7 @@
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"localbites": "file:", "localbites": "file:",
"lucide-react": "^0.436.0",
"next": "15.0.0-canary.104", "next": "15.0.0-canary.104",
"payload": "beta", "payload": "beta",
"react": "19.0.0-rc-06d0b89e-20240801", "react": "19.0.0-rc-06d0b89e-20240801",
@ -34,6 +35,7 @@
}, },
"devDependencies": { "devDependencies": {
"@pandacss/dev": "^0.45.1", "@pandacss/dev": "^0.45.1",
"@park-ui/panda-preset": "^0.42.0",
"@types/node": "^20.14.9", "@types/node": "^20.14.9",
"@types/react": "npm:types-react@19.0.0-rc.0", "@types/react": "npm:types-react@19.0.0-rc.0",
"@types/react-dom": "npm:types-react-dom@19.0.0-rc.0", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.0",

@ -1,9 +1,17 @@
import { createPreset } from "@park-ui/panda-preset";
import { defineConfig } from "@pandacss/dev"; import { defineConfig } from "@pandacss/dev";
export default defineConfig({ export default defineConfig({
// Whether to use css reset // Whether to use css reset
preflight: true, preflight: true,
presets: [
"@pandacss/preset-base",
createPreset({
accentColor: "red",
}),
],
// Where to look for your css declarations // Where to look for your css declarations
include: ["./src/**/*.{js,jsx,ts,tsx}", "./pages/**/*.{js,jsx,ts,tsx}"], include: ["./src/**/*.{js,jsx,ts,tsx}", "./pages/**/*.{js,jsx,ts,tsx}"],
@ -11,10 +19,10 @@ export default defineConfig({
exclude: [], exclude: [],
// Useful for theme customization // Useful for theme customization
theme: { theme: {},
extend: {},
},
// The output directory for your css system // The output directory for your css system
outdir: "styled-system", outdir: "styled-system",
jsxFramework: "react",
}); });

@ -0,0 +1,5 @@
{
"$schema": "https://park-ui.com/registry/latest/schema.json",
"jsFramework": "react",
"outputPath": "./src/components/ui"
}

@ -0,0 +1,10 @@
import Carousel from "@/components/ui/carousel";
import { Media } from "@/payload-types";
import { getPayload } from "@/utils/payload";
export default async function Gallery() {
const payload = await getPayload();
const { images } = await payload.findGlobal({ slug: "gallery" });
return <Carousel images={images.map(({ image }) => image as Media)} w="100%" />;
}

@ -1,9 +1,13 @@
import "./globals.css"; import "../globals.css";
import { Inter } from "next/font/google"; import Navbar from "@/components/layout/navbar";
import localFont from "next/font/local";
const inter = Inter({ subsets: ["latin"] }); import { styled } from "@styled-system/jsx";
const moderustic = localFont({
src: "./Moderustic-VariableFont_wght.ttf",
display: "swap",
});
export const metadata = { export const metadata = {
title: "Create Next App", title: "Create Next App",
description: "Generated by create next app", description: "Generated by create next app",
@ -12,7 +16,10 @@ export const metadata = {
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {
return ( return (
<html lang="en"> <html lang="en">
<body className={inter.className}>{children}</body> <styled.body className={moderustic.className} pb={20}>
<Navbar />
<styled.main mt={20}>{children}</styled.main>
</styled.body>
</html> </html>
); );
} }

@ -0,0 +1,54 @@
import { Box, Container, Stack } from "@styled-system/jsx";
import Gallery from "./gallery";
import Image from "next/image";
import { Media } from "@/payload-types";
import { Metadata } from "next";
import RichText from "@/components/rich-text";
import { css } from "@styled-system/css";
import { getPayload } from "@/utils/payload";
import { styled } from "@styled-system/jsx";
export default async function Home() {
const payload = await getPayload();
const home = await payload.findGlobal({ slug: "home" });
const about = await payload.findGlobal({ slug: "about" });
return (
<Stack gap={10} align="center">
<Box w="100%" h="80vh" position="relative">
<Image
src={(home.splashImage as Media)!.url!}
alt={about.name}
className={css({ objectFit: "cover" })}
fill
/>
<styled.div
position="absolute"
color="white"
top="70%"
width="100%"
textAlign="center"
fontSize={24}
>
{home.tagline}
</styled.div>
</Box>
<Container>
<RichText content={home.aboutText} />
</Container>
<Box maxW={600} w="100%" h="60vh">
<Gallery />
</Box>
</Stack>
);
}
export async function generateMetadata(): Promise<Metadata> {
const payload = await getPayload();
const about = await payload.findGlobal({ slug: "about" });
return {
title: about.name,
};
}

@ -1,7 +1,7 @@
import type { CollectionConfig } from "payload"; import type { CollectionConfig } from "payload";
export const MenuCategory: CollectionConfig = { export const Announcement: CollectionConfig = {
slug: "opening-time", slug: "announcement",
access: { access: {
read: () => true, read: () => true,
}, },

@ -0,0 +1,38 @@
import type { CollectionConfig } from "payload";
export const Holiday: CollectionConfig = {
slug: "holiday",
access: {
read: () => true,
},
admin: {
useAsTitle: "title",
},
fields: [
{
name: "title",
type: "text",
localized: true,
},
{
name: "from",
type: "date",
admin: {
date: {
pickerAppearance: "dayOnly",
displayFormat: "d MMM yyy",
},
},
},
{
name: "to",
type: "date",
admin: {
date: {
pickerAppearance: "dayOnly",
displayFormat: "d MMM yyy",
},
},
},
],
};

@ -9,7 +9,7 @@ export const Media: CollectionConfig = {
{ {
name: "alt", name: "alt",
type: "text", type: "text",
required: true, required: false,
}, },
], ],
upload: true, upload: true,

@ -38,5 +38,21 @@ export const MenuItem: CollectionConfig = {
relationTo: "menu-item-tag", relationTo: "menu-item-tag",
hasMany: true, hasMany: true,
}, },
{
name: "variants",
type: "array",
minRows: 1,
required: true,
fields: [
{
name: "title",
type: "text",
},
{
name: "price",
type: "number",
},
],
},
], ],
}; };

@ -0,0 +1,38 @@
import type { CollectionConfig } from "payload";
export const Vacation: CollectionConfig = {
slug: "vacation",
access: {
read: () => true,
},
admin: {
useAsTitle: "title",
},
fields: [
{
name: "title",
type: "text",
localized: true,
},
{
name: "from",
type: "date",
admin: {
date: {
pickerAppearance: "dayOnly",
displayFormat: "d MMM yyy",
},
},
},
{
name: "to",
type: "date",
admin: {
date: {
pickerAppearance: "dayOnly",
displayFormat: "d MMM yyy",
},
},
},
],
};

@ -0,0 +1,67 @@
import Image from "next/image";
import Link from "next/link";
import { Media } from "@/payload-types";
import { css } from "@styled-system/css";
import { flex } from "@styled-system/patterns";
import { getPayload } from "@/utils/payload";
import { styled } from "@styled-system/jsx";
export default async function Navbar() {
const payload = await getPayload();
const about = await payload.findGlobal({ slug: "about" });
return (
<styled.nav
className={flex({ gap: 4 })}
bg="white"
h={20}
px={4}
boxShadow="lg"
alignItems="center"
position="fixed"
zIndex={500}
top={0}
left={0}
right={0}
width="100%"
>
{about.logo ? (
<Link href="/" className={css({ h: 16, w: 200, position: "relative" })}>
<Image
src={(about.logo as Media).url!}
alt={(about.logo as Media).alt ?? about.name}
className={css({ objectFit: "contain", objectPosition: "left" })}
fill
/>
</Link>
) : (
<Link href="/" className={css({ fontSize: 24 })}>
{about.name}
</Link>
)}
<styled.div flexGrow={1} />
<Link href="/about" className={css({ _hover: { color: "gray.800" } })}>
Über uns
</Link>
<Link href="/menu" className={css({ _hover: { color: "gray.800" } })}>
Menü
</Link>
<Link
href="/contact"
className={css({
background: "accent.9",
color: "white",
p: 2,
borderRadius: "md",
_hover: {
background: "accent.11",
},
})}
>
Kontakt
</Link>
</styled.nav>
);
}

@ -0,0 +1,30 @@
import { css, cx } from "@styled-system/css";
import React from "react";
import { container } from "@styled-system/patterns";
import { serializeLexical } from "./serialize";
type Props = {
className?: string;
content: Record<string, any>;
enableGutter?: boolean;
enableProse?: boolean;
};
const RichText: React.FC<Props> = ({ className, content, enableGutter = true }) => {
if (!content) {
return null;
}
return (
<div className={cx(enableGutter ? container() : css({ maxW: "none" }), className)}>
{content &&
!Array.isArray(content) &&
typeof content === "object" &&
"root" in content &&
serializeLexical({ nodes: content?.root?.children })}
</div>
);
};
export default RichText;

@ -0,0 +1,125 @@
// @ts-nocheck
//This copy-and-pasted from lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
import type { ElementFormatType, TextFormatType } from "lexical";
import type { TextDetailType, TextModeType } from "lexical/nodes/LexicalTextNode";
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
// DOM
export const DOM_ELEMENT_TYPE = 1;
export const DOM_TEXT_TYPE = 3;
// Reconciling
export const NO_DIRTY_NODES = 0;
export const HAS_DIRTY_NODES = 1;
export const FULL_RECONCILE = 2;
// Text node modes
export const IS_NORMAL = 0;
export const IS_TOKEN = 1;
export const IS_SEGMENTED = 2;
// IS_INERT = 3
// Text node formatting
export const IS_BOLD = 1;
export const IS_ITALIC = 1 << 1;
export const IS_STRIKETHROUGH = 1 << 2;
export const IS_UNDERLINE = 1 << 3;
export const IS_CODE = 1 << 4;
export const IS_SUBSCRIPT = 1 << 5;
export const IS_SUPERSCRIPT = 1 << 6;
export const IS_HIGHLIGHT = 1 << 7;
export const IS_ALL_FORMATTING =
IS_BOLD |
IS_ITALIC |
IS_STRIKETHROUGH |
IS_UNDERLINE |
IS_CODE |
IS_SUBSCRIPT |
IS_SUPERSCRIPT |
IS_HIGHLIGHT;
// Text node details
export const IS_DIRECTIONLESS = 1;
export const IS_UNMERGEABLE = 1 << 1;
// Element node formatting
export const IS_ALIGN_LEFT = 1;
export const IS_ALIGN_CENTER = 2;
export const IS_ALIGN_RIGHT = 3;
export const IS_ALIGN_JUSTIFY = 4;
export const IS_ALIGN_START = 5;
export const IS_ALIGN_END = 6;
// Reconciliation
export const NON_BREAKING_SPACE = "\u00A0";
const ZERO_WIDTH_SPACE = "\u200b";
export const DOUBLE_LINE_BREAK = "\n\n";
// For FF, we need to use a non-breaking space, or it gets composition
// in a stuck state.
const RTL = "\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC";
const LTR =
"A-Za-z\u00C0-\u00D6\u00D8-\u00F6" +
"\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF\u200E\u2C00-\uFB1C" +
"\uFE00-\uFE6F\uFEFD-\uFFFF";
export const RTL_REGEX = new RegExp("^[^" + LTR + "]*[" + RTL + "]");
export const LTR_REGEX = new RegExp("^[^" + RTL + "]*[" + LTR + "]");
export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
bold: IS_BOLD,
code: IS_CODE,
highlight: IS_HIGHLIGHT,
italic: IS_ITALIC,
strikethrough: IS_STRIKETHROUGH,
subscript: IS_SUBSCRIPT,
superscript: IS_SUPERSCRIPT,
underline: IS_UNDERLINE,
};
export const DETAIL_TYPE_TO_DETAIL: Record<TextDetailType | string, number> = {
directionless: IS_DIRECTIONLESS,
unmergeable: IS_UNMERGEABLE,
};
export const ELEMENT_TYPE_TO_FORMAT: Record<Exclude<ElementFormatType, "">, number> = {
center: IS_ALIGN_CENTER,
end: IS_ALIGN_END,
justify: IS_ALIGN_JUSTIFY,
left: IS_ALIGN_LEFT,
right: IS_ALIGN_RIGHT,
start: IS_ALIGN_START,
};
export const ELEMENT_FORMAT_TO_TYPE: Record<number, ElementFormatType> = {
[IS_ALIGN_CENTER]: "center",
[IS_ALIGN_END]: "end",
[IS_ALIGN_JUSTIFY]: "justify",
[IS_ALIGN_LEFT]: "left",
[IS_ALIGN_RIGHT]: "right",
[IS_ALIGN_START]: "start",
};
export const TEXT_MODE_TO_TYPE: Record<TextModeType, 0 | 1 | 2> = {
normal: IS_NORMAL,
segmented: IS_SEGMENTED,
token: IS_TOKEN,
};
export const TEXT_TYPE_TO_MODE: Record<number, TextModeType> = {
[IS_NORMAL]: "normal",
[IS_SEGMENTED]: "segmented",
[IS_TOKEN]: "token",
};

@ -0,0 +1,159 @@
import { DefaultNodeTypes, SerializedBlockNode } from "@payloadcms/richtext-lexical";
import {
IS_BOLD,
IS_CODE,
IS_ITALIC,
IS_STRIKETHROUGH,
IS_SUBSCRIPT,
IS_SUPERSCRIPT,
IS_UNDERLINE,
} from "./node-format";
import React, { Fragment, JSX } from "react";
import Link from "next/link";
export type NodeTypes = DefaultNodeTypes;
type Props = {
nodes: NodeTypes[];
};
export function serializeLexical({ nodes }: Props): JSX.Element {
return (
<Fragment>
{nodes?.map((node, index): JSX.Element | null => {
if (node == null) {
return null;
}
if (node.type === "text") {
let text = <React.Fragment key={index}>{node.text}</React.Fragment>;
if (node.format & IS_BOLD) {
text = <strong key={index}>{text}</strong>;
}
if (node.format & IS_ITALIC) {
text = <em key={index}>{text}</em>;
}
if (node.format & IS_STRIKETHROUGH) {
text = (
<span key={index} style={{ textDecoration: "line-through" }}>
{text}
</span>
);
}
if (node.format & IS_UNDERLINE) {
text = (
<span key={index} style={{ textDecoration: "underline" }}>
{text}
</span>
);
}
if (node.format & IS_CODE) {
text = <code key={index}>{node.text}</code>;
}
if (node.format & IS_SUBSCRIPT) {
text = <sub key={index}>{text}</sub>;
}
if (node.format & IS_SUPERSCRIPT) {
text = <sup key={index}>{text}</sup>;
}
return text;
}
// NOTE: Hacky fix for
// https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
// which does not return checked: false (only true - i.e. there is no prop for false)
const serializedChildrenFn = (node: NodeTypes): JSX.Element | null => {
if (node.children == null) {
return null;
} else {
if (node?.type === "list" && node?.listType === "check") {
for (const item of node.children) {
if ("checked" in item) {
if (!item?.checked) {
item.checked = false;
}
}
}
}
return serializeLexical({ nodes: node.children as NodeTypes[] });
}
};
const serializedChildren = "children" in node ? serializedChildrenFn(node) : "";
switch (node.type) {
case "linebreak": {
return <br key={index} />;
}
case "paragraph": {
return (
<p key={index}>
{serializedChildren}
</p>
);
}
case "heading": {
const Tag = node?.tag;
return (
<Tag key={index}>
{serializedChildren}
</Tag>
);
}
case "list": {
const Tag = node?.tag;
return (
<Tag className="list col-start-2" key={index}>
{serializedChildren}
</Tag>
);
}
case "listitem": {
if (node?.checked != null) {
return (
<li
aria-checked={node.checked ? "true" : "false"}
className={` ${node.checked ? "" : ""}`}
key={index}
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role
role="checkbox"
tabIndex={-1}
value={node?.value}
>
{serializedChildren}
</li>
);
} else {
return (
<li key={index} value={node?.value}>
{serializedChildren}
</li>
);
}
}
case "quote": {
return (
<blockquote className="col-start-2" key={index}>
{serializedChildren}
</blockquote>
);
}
case "link": {
const fields = node.fields;
return (
<a key={index} href={fields.url} target={Boolean(fields?.newTab) ? "_blank" : ""}>
{serializedChildren}
</a>
);
}
default:
return null;
}
})}
</Fragment>
);
}

@ -0,0 +1,40 @@
import { forwardRef } from 'react'
import { Center, styled } from 'styled-system/jsx'
import { Spinner } from './spinner'
import { Button as StyledButton, type ButtonProps as StyledButtonProps } from './styled/button'
interface ButtonLoadingProps {
loading?: boolean
loadingText?: React.ReactNode
}
export interface ButtonProps extends StyledButtonProps, ButtonLoadingProps {}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const { loading, disabled, loadingText, children, ...rest } = props
const trulyDisabled = loading || disabled
return (
<StyledButton disabled={trulyDisabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<ButtonSpinner />
<styled.span opacity={0}>{children}</styled.span>
</>
) : loadingText ? (
loadingText
) : (
children
)}
</StyledButton>
)
})
Button.displayName = 'Button'
const ButtonSpinner = () => (
<Center inline position="absolute" transform="translate(-50%, -50%)" top="50%" insetStart="50%">
<Spinner colorPalette="gray" />
</Center>
)

@ -0,0 +1,63 @@
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import {
Control,
Indicator,
IndicatorGroup,
Item,
ItemGroup,
NextTrigger,
PrevTrigger,
Root,
RootProps,
Viewport,
} from "./styled/carousel";
import { IconButton } from "@/components/ui/icon-button";
import Image from "next/image";
import { Media } from "@/payload-types";
import { css } from "@styled-system/css";
import { styled } from "@styled-system/jsx";
export type CarouselProps = RootProps & {
images: Media[];
};
export default function Carousel({ images, ...props }: CarouselProps) {
return (
<Root h="100%" {...props}>
<Viewport h="100%">
<ItemGroup style={{ height: "100%" }}>
{images.map((image, index) => (
<Item key={index} index={index}>
<styled.div h="100%" width="100%" position="relative">
<Image
src={image.url!}
alt={image.alt ?? ""}
className={css({ objectFit: "contain" })}
fill
/>
</styled.div>
</Item>
))}
</ItemGroup>
<Control>
<PrevTrigger asChild>
<IconButton size="sm" variant="link" aria-label="Previous Slide">
<ChevronLeftIcon />
</IconButton>
</PrevTrigger>
<IndicatorGroup>
{images.map((_, index) => (
<Indicator key={index} index={index} aria-label={`Goto slide ${index + 1}`} />
))}
</IndicatorGroup>
<NextTrigger asChild>
<IconButton size="sm" variant="link" aria-label="Next Slide">
<ChevronRightIcon />
</IconButton>
</NextTrigger>
</Control>
</Viewport>
</Root>
);
}

@ -0,0 +1 @@
export { IconButton, type IconButtonProps } from './styled/icon-button'

@ -0,0 +1,29 @@
import { forwardRef } from 'react'
import { styled } from 'styled-system/jsx'
import { Spinner as StyledSpinner, type SpinnerProps as StyledSpinnerProps } from './styled/spinner'
export interface SpinnerProps extends StyledSpinnerProps {
/**
* For accessibility, it is important to add a fallback loading text.
* This text will be visible to screen readers.
* @default "Loading..."
*/
label?: string
}
export const Spinner = forwardRef<HTMLDivElement, SpinnerProps>((props, ref) => {
const { label = 'Loading...', ...rest } = props
return (
<StyledSpinner
ref={ref}
borderBottomColor="transparent"
borderLeftColor="transparent"
{...rest}
>
{label && <styled.span srOnly>{label}</styled.span>}
</StyledSpinner>
)
})
Spinner.displayName = 'Spinner'

@ -0,0 +1,7 @@
import { ark } from '@ark-ui/react/factory'
import { styled } from 'styled-system/jsx'
import { button } from 'styled-system/recipes'
import type { ComponentProps } from 'styled-system/types'
export type ButtonProps = ComponentProps<typeof Button>
export const Button = styled(ark.button, button)

@ -0,0 +1,62 @@
'use client'
import type { Assign } from '@ark-ui/react'
import { Carousel } from '@ark-ui/react/carousel'
import { type CarouselVariantProps, carousel } from 'styled-system/recipes'
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withProvider, withContext } = createStyleContext(carousel)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withProvider<
HTMLDivElement,
Assign<Assign<HTMLStyledProps<'div'>, Carousel.RootProviderBaseProps>, CarouselVariantProps>
>(Carousel.RootProvider, 'root')
export type RootProps = ComponentProps<typeof Root>
export const Root = withProvider<
HTMLDivElement,
Assign<Assign<HTMLStyledProps<'div'>, Carousel.RootBaseProps>, CarouselVariantProps>
>(Carousel.Root, 'root')
export const Control = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Carousel.ControlBaseProps>
>(Carousel.Control, 'control')
export const IndicatorGroup = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Carousel.IndicatorGroupBaseProps>
>(Carousel.IndicatorGroup, 'indicatorGroup')
export const Indicator = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, Carousel.IndicatorBaseProps>
>(Carousel.Indicator, 'indicator')
export const ItemGroup = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Carousel.ItemGroupBaseProps>
>(Carousel.ItemGroup, 'itemGroup')
export const Item = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Carousel.ItemBaseProps>
>(Carousel.Item, 'item')
export const NextTrigger = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, Carousel.NextTriggerBaseProps>
>(Carousel.NextTrigger, 'nextTrigger')
export const PrevTrigger = withContext<
HTMLButtonElement,
Assign<HTMLStyledProps<'button'>, Carousel.PrevTriggerBaseProps>
>(Carousel.PrevTrigger, 'prevTrigger')
export const Viewport = withContext<
HTMLDivElement,
Assign<HTMLStyledProps<'div'>, Carousel.ViewportBaseProps>
>(Carousel.Viewport, 'viewport')
export { CarouselContext as Context } from '@ark-ui/react/carousel'

@ -0,0 +1,9 @@
import { ark } from '@ark-ui/react/factory'
import { styled } from 'styled-system/jsx'
import { type ButtonVariantProps, button } from 'styled-system/recipes'
import type { ComponentProps } from 'styled-system/types'
export type IconButtonProps = ComponentProps<typeof IconButton>
export const IconButton = styled(ark.button, button, {
defaultProps: { px: '0' } as ButtonVariantProps,
})

@ -0,0 +1,7 @@
import { ark } from '@ark-ui/react/factory'
import { styled } from 'styled-system/jsx'
import { spinner } from 'styled-system/recipes'
import type { ComponentProps } from 'styled-system/types'
export type SpinnerProps = ComponentProps<typeof Spinner>
export const Spinner = styled(ark.div, spinner)

@ -0,0 +1,95 @@
import {
type ElementType,
type ForwardRefExoticComponent,
type PropsWithoutRef,
type RefAttributes,
createContext,
forwardRef,
useContext,
} from 'react'
import { cx } from 'styled-system/css'
import { type StyledComponent, isCssProperty, styled } from 'styled-system/jsx'
type Props = Record<string, unknown>
type Recipe = {
(props?: Props): Props
splitVariantProps: (props: Props) => [Props, Props]
}
type Slot<R extends Recipe> = keyof ReturnType<R>
type Options = { forwardProps?: string[] }
const shouldForwardProp = (prop: string, variantKeys: string[], options: Options = {}) =>
options.forwardProps?.includes(prop) || (!variantKeys.includes(prop) && !isCssProperty(prop))
export const createStyleContext = <R extends Recipe>(recipe: R) => {
const StyleContext = createContext<Record<Slot<R>, string> | null>(null)
const withRootProvider = <P extends {}>(Component: ElementType) => {
const StyledComponent = (props: P) => {
const [variantProps, otherProps] = recipe.splitVariantProps(props)
const slotStyles = recipe(variantProps) as Record<Slot<R>, string>
return (
<StyleContext.Provider value={slotStyles}>
<Component {...otherProps} />
</StyleContext.Provider>
)
}
return StyledComponent
}
const withProvider = <T, P extends { className?: string | undefined }>(
Component: ElementType,
slot: Slot<R>,
options?: Options,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> => {
const StyledComponent = styled(
Component,
{},
{
shouldForwardProp: (prop, variantKeys) => shouldForwardProp(prop, variantKeys, options),
},
) as StyledComponent<ElementType>
const StyledSlotProvider = forwardRef<T, P>((props, ref) => {
const [variantProps, otherProps] = recipe.splitVariantProps(props)
const slotStyles = recipe(variantProps) as Record<Slot<R>, string>
return (
<StyleContext.Provider value={slotStyles}>
<StyledComponent
{...otherProps}
ref={ref}
className={cx(slotStyles?.[slot], props.className)}
/>
</StyleContext.Provider>
)
})
// @ts-expect-error
StyledSlotProvider.displayName = Component.displayName || Component.name
return StyledSlotProvider
}
const withContext = <T, P extends { className?: string | undefined }>(
Component: ElementType,
slot: Slot<R>,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>> => {
const StyledComponent = styled(Component)
const StyledSlotComponent = forwardRef<T, P>((props, ref) => {
const slotStyles = useContext(StyleContext)
return (
<StyledComponent {...props} ref={ref} className={cx(slotStyles?.[slot], props.className)} />
)
})
// @ts-expect-error
StyledSlotComponent.displayName = Component.displayName || Component.name
return StyledSlotComponent
}
return {
withRootProvider,
withProvider,
withContext,
}
}

@ -11,6 +11,11 @@ export const About: GlobalConfig = {
type: "text", type: "text",
required: true, required: true,
}, },
{
name: "logo",
type: "relationship",
relationTo: "media",
},
{ {
name: "text", name: "text",
type: "richText", type: "richText",

@ -6,6 +6,14 @@ export const Contact: GlobalConfig = {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{
name: "email",
type: "email",
},
{
name: "phone",
type: "text",
},
{ {
name: "address", name: "address",
type: "group", type: "group",

@ -0,0 +1,23 @@
import type { GlobalConfig } from "payload";
export const Gallery: GlobalConfig = {
slug: "gallery",
access: {
read: () => true,
},
fields: [
{
name: "images",
type: "array",
fields: [
{
name: "image",
type: "relationship",
relationTo: "media",
required: true,
},
],
required: true,
},
],
};

@ -0,0 +1,26 @@
import type { GlobalConfig } from "payload";
export const Home: GlobalConfig = {
slug: "home",
access: {
read: () => true,
},
fields: [
{
name: "splashImage",
type: "relationship",
relationTo: "media",
required: true,
},
{
name: "tagline",
type: "text",
required: true,
},
{
name: "aboutText",
type: "richText",
required: true,
},
],
};

@ -18,6 +18,8 @@ export interface Config {
'menu-category': MenuCategory; 'menu-category': MenuCategory;
'menu-item-tag': MenuItemTag; 'menu-item-tag': MenuItemTag;
'food-declaration': FoodDeclaration; 'food-declaration': FoodDeclaration;
vacation: Vacation;
holiday: Holiday;
'payload-preferences': PayloadPreference; 'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration; 'payload-migrations': PayloadMigration;
}; };
@ -25,6 +27,8 @@ export interface Config {
defaultIDType: string; defaultIDType: string;
}; };
globals: { globals: {
home: Home;
gallery: Gallery;
about: About; about: About;
contact: Contact; contact: Contact;
menu: Menu; menu: Menu;
@ -75,7 +79,7 @@ export interface User {
*/ */
export interface Media { export interface Media {
id: string; id: string;
alt: string; alt?: string | null;
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
url?: string | null; url?: string | null;
@ -126,6 +130,11 @@ export interface MenuItem {
image?: (string | null) | Media; image?: (string | null) | Media;
category?: (string | null) | MenuCategory; category?: (string | null) | MenuCategory;
tags?: (string | MenuItemTag)[] | null; tags?: (string | MenuItemTag)[] | null;
variants: {
title?: string | null;
price?: number | null;
id?: string | null;
}[];
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
@ -189,6 +198,30 @@ export interface FoodDeclaration {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "vacation".
*/
export interface Vacation {
id: string;
title?: string | null;
from?: string | null;
to?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "holiday".
*/
export interface Holiday {
id: string;
title?: string | null;
from?: string | null;
to?: string | null;
updatedAt: string;
createdAt: string;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences". * via the `definition` "payload-preferences".
@ -223,6 +256,45 @@ export interface PayloadMigration {
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "home".
*/
export interface Home {
id: string;
splashImage: string | Media;
tagline: string;
aboutText: {
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;
};
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "gallery".
*/
export interface Gallery {
id: string;
images: {
image: string | Media;
id?: string | null;
}[];
updatedAt?: string | null;
createdAt?: string | null;
}
/** /**
* This interface was referenced by `Config`'s JSON-Schema * This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "about". * via the `definition` "about".
@ -230,6 +302,7 @@ export interface PayloadMigration {
export interface About { export interface About {
id: string; id: string;
name: string; name: string;
logo?: (string | null) | Media;
text?: { text?: {
root: { root: {
type: string; type: string;
@ -254,6 +327,8 @@ export interface About {
*/ */
export interface Contact { export interface Contact {
id: string; id: string;
email?: string | null;
phone?: string | null;
address: { address: {
street: string; street: string;
number: number; number: number;

@ -3,6 +3,9 @@ import { DefaultTranslationsObject, Language } from "@payloadcms/translations";
import { About } from "./globals/About"; import { About } from "./globals/About";
import { Contact } from "./globals/Contact"; import { Contact } from "./globals/Contact";
import { FoodDeclaration } from "./collections/FoodDeclaration"; import { FoodDeclaration } from "./collections/FoodDeclaration";
import { Gallery } from "@/globals/Gallery";
import { Holiday } from "./collections/Holiday";
import { Home } from "./globals/Home";
import { Media } from "./collections/Media"; import { Media } from "./collections/Media";
import { Menu } from "./globals/Menu"; import { Menu } from "./globals/Menu";
import { MenuCategory } from "./collections/MenuCategory"; import { MenuCategory } from "./collections/MenuCategory";
@ -10,6 +13,7 @@ import { MenuItem } from "./collections/MenuItem";
import { MenuItemTag } from "./collections/MenuItemTag"; import { MenuItemTag } from "./collections/MenuItemTag";
import { OpeningTime } from "./collections/OpeningTime"; import { OpeningTime } from "./collections/OpeningTime";
import { Users } from "./collections/Users"; import { Users } from "./collections/Users";
import { Vacation } from "./collections/Vacation";
import { buildConfig } from "payload"; import { buildConfig } from "payload";
import { de } from "@payloadcms/translations/languages/de"; import { de } from "@payloadcms/translations/languages/de";
import { en } from "@payloadcms/translations/languages/en"; import { en } from "@payloadcms/translations/languages/en";
@ -32,8 +36,24 @@ export default buildConfig({
baseDir: path.resolve(dirname), baseDir: path.resolve(dirname),
}, },
}, },
collections: [Users, Media, OpeningTime, MenuItem, MenuCategory, MenuItemTag, FoodDeclaration], collections: [
globals: [About, Contact, Menu], Users,
Media,
OpeningTime,
MenuItem,
MenuCategory,
MenuItemTag,
FoodDeclaration,
Vacation,
Holiday,
],
globals: [
Home,
Gallery,
About,
Contact,
Menu,
],
editor: lexicalEditor(), editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET || "", secret: process.env.PAYLOAD_SECRET || "",
typescript: { typescript: {

@ -0,0 +1,4 @@
import config from "@payload-config";
import { getPayloadHMR } from "@payloadcms/next/utilities";
export const getPayload = async () => await getPayloadHMR({ config });

@ -26,6 +26,9 @@
"@/*": [ "@/*": [
"./src/*" "./src/*"
], ],
"@styled-system/*": [
"./styled-system/*"
],
"@payload-config": [ "@payload-config": [
"./src/payload.config.ts" "./src/payload.config.ts"
] ]

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save