feat: ✨ implement simple landing page with image, tagline, short about text and image gallery
- Add ParkUI - Add lucide-react - Add RichText renderer - Add Moderustic fontmain
parent
c3dd300769
commit
34be1c9ad7
@ -0,0 +1,4 @@
|
||||
{
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative"
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "https://park-ui.com/registry/latest/schema.json",
|
||||
"jsFramework": "react",
|
||||
"outputPath": "./src/components/ui"
|
||||
}
|
Binary file not shown.
@ -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%" />;
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
@ -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,
|
||||
}
|
||||
}
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
import config from "@payload-config";
|
||||
import { getPayloadHMR } from "@payloadcms/next/utilities";
|
||||
|
||||
export const getPayload = async () => await getPayloadHMR({ config });
|
Loading…
Reference in New Issue