diff --git a/apps/demo/app/[...puckPath]/client.tsx b/apps/demo/app/[...puckPath]/client.tsx index e681552754..dcc983051b 100644 --- a/apps/demo/app/[...puckPath]/client.tsx +++ b/apps/demo/app/[...puckPath]/client.tsx @@ -5,11 +5,11 @@ import headingAnalyzer from "@/plugin-heading-analyzer/src/HeadingAnalyzer"; import config from "../../config"; import { useDemoData } from "../../lib/use-demo-data"; import { useEffect, useState } from "react"; -import dynamic from "next/dynamic"; -import { useMetadata } from "../../lib/use-metadata"; export function Client({ path, isEdit }: { path: string; isEdit: boolean }) { - const { metadata } = useMetadata(); + const metadata = { + example: "Hello, world", + }; const { data, resolvedData, key } = useDemoData({ path, diff --git a/apps/demo/app/custom-ui/[...puckPath]/client.tsx b/apps/demo/app/custom-ui/[...puckPath]/client.tsx index f9fd5bf3c2..0a07c9d12c 100644 --- a/apps/demo/app/custom-ui/[...puckPath]/client.tsx +++ b/apps/demo/app/custom-ui/[...puckPath]/client.tsx @@ -5,7 +5,8 @@ import { ActionBar, Button, Data, Puck, Render } from "@/core"; import { HeadingAnalyzer } from "@/plugin-heading-analyzer/src/HeadingAnalyzer"; -import config, { UserConfig } from "../../../config"; +import config from "../../../config"; +import { UserConfig } from "../../../config/types"; import { useDemoData } from "../../../lib/use-demo-data"; import { IconButton, createUsePuck } from "@/core"; import { ReactNode, useEffect, useRef, useState } from "react"; diff --git a/apps/demo/app/rsc/page.tsx b/apps/demo/app/rsc/page.tsx new file mode 100644 index 0000000000..2e82aeff37 --- /dev/null +++ b/apps/demo/app/rsc/page.tsx @@ -0,0 +1,32 @@ +import { Metadata } from "next"; +import config from "../../config/server"; +import { initialData } from "../../config/initial-data"; +import { Props, RootProps } from "../../config/types"; + +import { Config } from "@/core"; +import { Render, resolveAllData } from "@/core/rsc"; + +// NB This is only necessary for this demo app, as the `@/core/rsc` path does not resolve to dist but the type for Config does +// This will be resolved once the RSC package is merged with the regular package after DropZone support is dropped +const conf = config as unknown as Config; + +export async function generateMetadata(): Promise { + return { + title: initialData["/"].root.title, + }; +} + +export default async function Page() { + const data = initialData["/"]; + const metadata = { + example: "Hello, world", + }; + + const resolvedData = await resolveAllData( + data, + conf, + metadata + ); + + return ; +} diff --git a/apps/demo/config/blocks/Hero/Hero.tsx b/apps/demo/config/blocks/Hero/Hero.tsx new file mode 100644 index 0000000000..e6736496c2 --- /dev/null +++ b/apps/demo/config/blocks/Hero/Hero.tsx @@ -0,0 +1,97 @@ +/* eslint-disable @next/next/no-img-element */ +import React from "react"; +import styles from "./styles.module.css"; +import { getClassNameFactory } from "@/core/lib"; +import { Button } from "@/core/components/Button"; +import { Section } from "../../components/Section"; +import { PuckComponent } from "@/core/types"; + +const getClassName = getClassNameFactory("Hero", styles); + +export type HeroProps = { + quote?: { index: number; label: string }; + title: string; + description: string; + align?: string; + padding: string; + image?: { + mode?: "inline" | "background"; + url?: string; + }; + buttons: { + label: string; + href: string; + variant?: "primary" | "secondary"; + }[]; +}; + +export const Hero: PuckComponent = ({ + align, + title, + description, + buttons, + padding, + image, + puck, +}) => { + return ( +
+ {image?.mode === "background" && ( + <> +
+ +
+ + )} + +
+
+

{title}

+

{description}

+
+ {buttons.map((button, i) => ( + + ))} +
+
+ + {align !== "center" && image?.mode !== "background" && image?.url && ( +
+ )} +
+
+ ); +}; + +export default Hero; diff --git a/apps/demo/config/blocks/Hero/client.tsx b/apps/demo/config/blocks/Hero/client.tsx new file mode 100644 index 0000000000..9046544c19 --- /dev/null +++ b/apps/demo/config/blocks/Hero/client.tsx @@ -0,0 +1,195 @@ +/* eslint-disable @next/next/no-img-element */ +import React from "react"; +import { ComponentConfig } from "@/core/types"; +import { quotes } from "./quotes"; +import { AutoField, FieldLabel } from "@/core"; +import { Link2 } from "lucide-react"; +import HeroComponent, { HeroProps } from "./Hero"; + +export const Hero: ComponentConfig = { + fields: { + quote: { + type: "external", + placeholder: "Select a quote", + showSearch: false, + renderFooter: ({ items }) => { + return ( +
+ {items.length} result{items.length === 1 ? "" : "s"} +
+ ); + }, + filterFields: { + author: { + type: "select", + options: [ + { value: "", label: "Select an author" }, + { value: "Mark Twain", label: "Mark Twain" }, + { value: "Henry Ford", label: "Henry Ford" }, + { value: "Kurt Vonnegut", label: "Kurt Vonnegut" }, + { value: "Andrew Carnegie", label: "Andrew Carnegie" }, + { value: "C. S. Lewis", label: "C. S. Lewis" }, + { value: "Confucius", label: "Confucius" }, + { value: "Eleanor Roosevelt", label: "Eleanor Roosevelt" }, + { value: "Samuel Ullman", label: "Samuel Ullman" }, + ], + }, + }, + fetchList: async ({ query, filters }) => { + // Simulate delay + await new Promise((res) => setTimeout(res, 500)); + + return quotes + .map((quote, idx) => ({ + index: idx, + title: quote.author, + description: quote.content, + })) + .filter((item) => { + if (filters?.author && item.title !== filters?.author) { + return false; + } + + if (!query) return true; + + const queryLowercase = query.toLowerCase(); + + if (item.title.toLowerCase().indexOf(queryLowercase) > -1) { + return true; + } + + if (item.description.toLowerCase().indexOf(queryLowercase) > -1) { + return true; + } + }); + }, + mapRow: (item) => ({ + title: item.title, + description: {item.description}, + }), + mapProp: (result) => { + return { index: result.index, label: result.description }; + }, + getItemSummary: (item) => item.label, + }, + title: { type: "text" }, + description: { type: "textarea" }, + buttons: { + type: "array", + min: 1, + max: 4, + getItemSummary: (item) => item.label || "Button", + arrayFields: { + label: { type: "text" }, + href: { type: "text" }, + variant: { + type: "select", + options: [ + { label: "primary", value: "primary" }, + { label: "secondary", value: "secondary" }, + ], + }, + }, + defaultItemProps: { + label: "Button", + href: "#", + }, + }, + align: { + type: "radio", + options: [ + { label: "left", value: "left" }, + { label: "center", value: "center" }, + ], + }, + image: { + type: "object", + objectFields: { + url: { + type: "custom", + render: ({ value, field, name, onChange, readOnly }) => ( + } + > + + + ), + }, + mode: { + type: "radio", + options: [ + { label: "inline", value: "inline" }, + { label: "background", value: "background" }, + ], + }, + }, + }, + padding: { type: "text" }, + }, + defaultProps: { + title: "Hero", + align: "left", + description: "Description", + buttons: [{ label: "Learn more", href: "#" }], + padding: "64px", + }, + /** + * The resolveData method allows us to modify component data after being + * set by the user. + * + * It is called after the page data is changed, but before a component + * is rendered. This allows us to make dynamic changes to the props + * without storing the data in Puck. + * + * For example, requesting a third-party API for the latest content. + */ + resolveData: async ({ props }, { changed }) => { + if (!props.quote) + return { props, readOnly: { title: false, description: false } }; + + if (!changed.quote) { + return { props }; + } + + // Simulate a delay + await new Promise((resolve) => setTimeout(resolve, 500)); + + return { + props: { + title: quotes[props.quote.index].author, + description: quotes[props.quote.index].content, + }, + readOnly: { title: true, description: true }, + }; + }, + resolveFields: async (data, { fields }) => { + if (data.props.align === "center") { + return { + ...fields, + image: undefined, + }; + } + + return fields; + }, + resolvePermissions: async (data, params) => { + if (!params.changed.quote) return params.lastPermissions; + + // Simulate delay + await new Promise((resolve) => setTimeout(resolve, 500)); + + return { + ...params.permissions, + // Disable delete if quote 7 is selected + delete: data.props.quote?.index !== 7, + }; + }, + render: HeroComponent, +}; diff --git a/apps/demo/config/blocks/Hero/index.tsx b/apps/demo/config/blocks/Hero/index.tsx index 80cd76f79f..3ec2deb180 100644 --- a/apps/demo/config/blocks/Hero/index.tsx +++ b/apps/demo/config/blocks/Hero/index.tsx @@ -1,280 +1,2 @@ -/* eslint-disable @next/next/no-img-element */ -import React, { useState } from "react"; -import { ComponentConfig } from "@/core/types"; -import styles from "./styles.module.css"; -import { getClassNameFactory } from "@/core/lib"; -import { Button } from "@/core/components/Button"; -import { Section } from "../../components/Section"; -import { quotes } from "./quotes"; -import { AutoField, FieldLabel } from "@/core"; -import { Link2 } from "lucide-react"; - -const getClassName = getClassNameFactory("Hero", styles); - -export type HeroProps = { - quote?: { index: number; label: string }; - title: string; - description: string; - align?: string; - padding: string; - image?: { - mode?: "inline" | "background"; - url?: string; - }; - buttons: { - label: string; - href: string; - variant?: "primary" | "secondary"; - }[]; -}; - -export const Hero: ComponentConfig = { - fields: { - quote: { - type: "external", - placeholder: "Select a quote", - showSearch: false, - renderFooter: ({ items }) => { - return ( -
- {items.length} result{items.length === 1 ? "" : "s"} -
- ); - }, - filterFields: { - author: { - type: "select", - options: [ - { value: "", label: "Select an author" }, - { value: "Mark Twain", label: "Mark Twain" }, - { value: "Henry Ford", label: "Henry Ford" }, - { value: "Kurt Vonnegut", label: "Kurt Vonnegut" }, - { value: "Andrew Carnegie", label: "Andrew Carnegie" }, - { value: "C. S. Lewis", label: "C. S. Lewis" }, - { value: "Confucius", label: "Confucius" }, - { value: "Eleanor Roosevelt", label: "Eleanor Roosevelt" }, - { value: "Samuel Ullman", label: "Samuel Ullman" }, - ], - }, - }, - fetchList: async ({ query, filters }) => { - // Simulate delay - await new Promise((res) => setTimeout(res, 500)); - - return quotes - .map((quote, idx) => ({ - index: idx, - title: quote.author, - description: quote.content, - })) - .filter((item) => { - if (filters?.author && item.title !== filters?.author) { - return false; - } - - if (!query) return true; - - const queryLowercase = query.toLowerCase(); - - if (item.title.toLowerCase().indexOf(queryLowercase) > -1) { - return true; - } - - if (item.description.toLowerCase().indexOf(queryLowercase) > -1) { - return true; - } - }); - }, - mapRow: (item) => ({ - title: item.title, - description: {item.description}, - }), - mapProp: (result) => { - return { index: result.index, label: result.description }; - }, - getItemSummary: (item) => item.label, - }, - title: { type: "text" }, - description: { type: "textarea" }, - buttons: { - type: "array", - min: 1, - max: 4, - getItemSummary: (item) => item.label || "Button", - arrayFields: { - label: { type: "text" }, - href: { type: "text" }, - variant: { - type: "select", - options: [ - { label: "primary", value: "primary" }, - { label: "secondary", value: "secondary" }, - ], - }, - }, - defaultItemProps: { - label: "Button", - href: "#", - }, - }, - align: { - type: "radio", - options: [ - { label: "left", value: "left" }, - { label: "center", value: "center" }, - ], - }, - image: { - type: "object", - objectFields: { - url: { - type: "custom", - render: ({ value, field, name, onChange, readOnly }) => ( - } - > - - - ), - }, - mode: { - type: "radio", - options: [ - { label: "inline", value: "inline" }, - { label: "background", value: "background" }, - ], - }, - }, - }, - padding: { type: "text" }, - }, - defaultProps: { - title: "Hero", - align: "left", - description: "Description", - buttons: [{ label: "Learn more", href: "#" }], - padding: "64px", - }, - /** - * The resolveData method allows us to modify component data after being - * set by the user. - * - * It is called after the page data is changed, but before a component - * is rendered. This allows us to make dynamic changes to the props - * without storing the data in Puck. - * - * For example, requesting a third-party API for the latest content. - */ - resolveData: async ({ props }, { changed }) => { - if (!props.quote) - return { props, readOnly: { title: false, description: false } }; - - if (!changed.quote) { - return { props }; - } - - // Simulate a delay - await new Promise((resolve) => setTimeout(resolve, 500)); - - return { - props: { - title: quotes[props.quote.index].author, - description: quotes[props.quote.index].content, - }, - readOnly: { title: true, description: true }, - }; - }, - resolveFields: async (data, { fields }) => { - if (data.props.align === "center") { - return { - ...fields, - image: undefined, - }; - } - - return fields; - }, - resolvePermissions: async (data, params) => { - if (!params.changed.quote) return params.lastPermissions; - - // Simulate delay - await new Promise((resolve) => setTimeout(resolve, 500)); - - return { - ...params.permissions, - // Disable delete if quote 7 is selected - delete: data.props.quote?.index !== 7, - }; - }, - render: ({ align, title, description, buttons, padding, image, puck }) => { - // Empty state allows us to test that components support hooks - // eslint-disable-next-line react-hooks/rules-of-hooks - const [_] = useState(0); - - return ( -
- {image?.mode === "background" && ( - <> -
- -
- - )} - -
-
-

{title}

-

{description}

-
- {buttons.map((button, i) => ( - - ))} -
-
- - {align !== "center" && image?.mode !== "background" && image?.url && ( -
- )} -
-
- ); - }, -}; +export * from "./client"; +export { type HeroProps } from "./Hero"; diff --git a/apps/demo/config/blocks/Hero/server.tsx b/apps/demo/config/blocks/Hero/server.tsx new file mode 100644 index 0000000000..b2cdefbb08 --- /dev/null +++ b/apps/demo/config/blocks/Hero/server.tsx @@ -0,0 +1,7 @@ +/* eslint-disable @next/next/no-img-element */ +import { ComponentConfig } from "@/core/types"; +import HeroComponent, { HeroProps } from "./Hero"; + +export const Hero: ComponentConfig = { + render: HeroComponent, +}; diff --git a/apps/demo/config/blocks/Template/Template.tsx b/apps/demo/config/blocks/Template/Template.tsx new file mode 100644 index 0000000000..2ea944437e --- /dev/null +++ b/apps/demo/config/blocks/Template/Template.tsx @@ -0,0 +1,25 @@ +import React from "react"; +import { Slot } from "@/core/types"; +import styles from "./styles.module.css"; +import { getClassNameFactory } from "@/core/lib"; +import { Section } from "../../components/Section"; +import { PuckComponent } from "@/core/types"; + +const getClassName = getClassNameFactory("Template", styles); + +export type TemplateProps = { + template: string; + children: Slot; +}; + +export const Template: PuckComponent = ({ + children: Children, +}) => { + return ( +
+ +
+ ); +}; + +export default Template; diff --git a/apps/demo/config/blocks/Template/client.tsx b/apps/demo/config/blocks/Template/client.tsx new file mode 100644 index 0000000000..13a1231d68 --- /dev/null +++ b/apps/demo/config/blocks/Template/client.tsx @@ -0,0 +1,197 @@ +/* eslint-disable react-hooks/rules-of-hooks */ +import React, { useState } from "react"; +import { AutoField, Button, createUsePuck, FieldLabel } from "@/core"; +import { ComponentConfig, ComponentData, Slot } from "@/core/types"; +import { withLayout } from "../../components/Layout"; +import { generateId } from "@/core/lib/generate-id"; +import { componentKey } from "../../index"; +import { type Props } from "../../types"; +import { mapSlotsAsync } from "@/core/lib/data/map-slots"; +import TemplateComponent, { TemplateProps } from "./Template"; + +const usePuck = createUsePuck(); + +async function createComponent( + component: T, + props?: Partial +) { + const { conf: config } = await import("../../index"); + + return { + type: component, + props: { + ...config.components[component].defaultProps, + ...props, + id: generateId(component), + }, + }; +} + +type TemplateData = Record; + +export const TemplateInternal: ComponentConfig = { + fields: { + template: { + type: "custom", + render: ({ name, value, onChange }) => { + const templateKey = `puck-demo-templates:${componentKey}`; + + const props: TemplateProps | undefined = usePuck( + (s) => s.selectedItem?.props + ); + + const [templates, setTemplates] = useState( + JSON.parse(localStorage.getItem(templateKey) ?? "{}") + ); + + return ( + + ({ + value: key, + label: template.label, + })), + ], + }} + /> +
+ +
+
+ ); + }, + }, + children: { + type: "slot", + }, + }, + defaultProps: { + template: "example_1", + children: [], + }, + resolveData: async (data, { changed, trigger }) => { + if (!changed.template || trigger === "load") return data; + + const templateKey = `puck-demo-templates:${componentKey}`; + + const templates: TemplateData = { + ...JSON.parse(localStorage.getItem(templateKey) ?? "{}"), + blank: { + label: "Blank", + data: [], + }, + example_1: { + label: "Example 1", + data: [ + await createComponent("Heading", { + text: "Template example.", + size: "xl", + }), + await createComponent("Text", { + text: "This component uses the slots API. Try changing template, or saving a new one via the template field.", + }), + ], + }, + example_2: { + label: "Example 2", + data: [ + await createComponent("Grid", { + numColumns: 2, + items: [ + await createComponent("Card", { title: "A card", mode: "card" }), + await createComponent("Flex", { + direction: "column", + gap: 0, + items: [ + await createComponent("Space", { + size: "32px", + }), + await createComponent("Heading", { + text: "Template example", + size: "xl", + }), + await createComponent("Text", { + text: "Dynamically create components using the new slots API.", + }), + await createComponent("Space", { + size: "16px", + }), + await createComponent("Button", { + variant: "secondary", + label: "Learn more", + }), + await createComponent("Space", { + size: "32px", + }), + ], + }), + ], + }), + ], + }, + }; + + let children = + templates[data.props.template]?.data || templates["example_1"].data; + + const randomizeId = (item: ComponentData) => ({ + ...item, + props: { ...item.props, id: generateId(item.type) }, + }); + + children = await Promise.all( + children.map((item) => + mapSlotsAsync(randomizeId(item), async (content) => + content.map(randomizeId) + ) + ) + ); + + return { + ...data, + props: { + ...data.props, + children, + }, + }; + }, + render: TemplateComponent, +}; + +export const Template = withLayout(TemplateInternal); diff --git a/apps/demo/config/blocks/Template/index.tsx b/apps/demo/config/blocks/Template/index.tsx index 03080b3b65..f14d13d52d 100644 --- a/apps/demo/config/blocks/Template/index.tsx +++ b/apps/demo/config/blocks/Template/index.tsx @@ -1,211 +1,2 @@ -/* eslint-disable react-hooks/rules-of-hooks */ -import React, { useState } from "react"; -import { ComponentConfig, ComponentData, Slot } from "@/core/types"; -import styles from "./styles.module.css"; -import { getClassNameFactory } from "@/core/lib"; -import { Section } from "../../components/Section"; -import { withLayout } from "../../components/Layout"; -import { generateId } from "@/core/lib/generate-id"; -import { componentKey, type Props } from "../../index"; -import { AutoField, Button, createUsePuck, FieldLabel } from "@/core"; -import { mapSlotsAsync } from "@/core/lib/data/map-slots"; - -const usePuck = createUsePuck(); - -async function createComponent( - component: T, - props?: Partial -) { - const { conf: config } = await import("../../index"); - - return { - type: component, - props: { - ...config.components[component].defaultProps, - ...props, - id: generateId(component), - }, - }; -} - -const getClassName = getClassNameFactory("Template", styles); - -export type TemplateProps = { - template: string; - children: Slot; -}; - -type TemplateData = Record; - -export const TemplateInternal: ComponentConfig = { - fields: { - template: { - type: "custom", - render: ({ name, value, onChange }) => { - const templateKey = `puck-demo-templates:${componentKey}`; - - const props: TemplateProps | undefined = usePuck( - (s) => s.selectedItem?.props - ); - - const [templates, setTemplates] = useState( - JSON.parse(localStorage.getItem(templateKey) ?? "{}") - ); - - return ( - - ({ - value: key, - label: template.label, - })), - ], - }} - /> -
- -
-
- ); - }, - }, - children: { - type: "slot", - }, - }, - defaultProps: { - template: "example_1", - children: [], - }, - resolveData: async (data, { changed, trigger }) => { - if (!changed.template || trigger === "load") return data; - - const templateKey = `puck-demo-templates:${componentKey}`; - - const templates: TemplateData = { - ...JSON.parse(localStorage.getItem(templateKey) ?? "{}"), - blank: { - label: "Blank", - data: [], - }, - example_1: { - label: "Example 1", - data: [ - await createComponent("Heading", { - text: "Template example.", - size: "xl", - }), - await createComponent("Text", { - text: "This component uses the slots API. Try changing template, or saving a new one via the template field.", - }), - ], - }, - example_2: { - label: "Example 2", - data: [ - await createComponent("Grid", { - numColumns: 2, - items: [ - await createComponent("Card", { title: "A card", mode: "card" }), - await createComponent("Flex", { - direction: "column", - gap: 0, - items: [ - await createComponent("Space", { - size: "32px", - }), - await createComponent("Heading", { - text: "Template example", - size: "xl", - }), - await createComponent("Text", { - text: "Dynamically create components using the new slots API.", - }), - await createComponent("Space", { - size: "16px", - }), - await createComponent("Button", { - variant: "secondary", - label: "Learn more", - }), - await createComponent("Space", { - size: "32px", - }), - ], - }), - ], - }), - ], - }, - }; - - let children = - templates[data.props.template]?.data || templates["example_1"].data; - - const randomizeId = (item: ComponentData) => ({ - ...item, - props: { ...item.props, id: generateId(item.type) }, - }); - - children = await Promise.all( - children.map((item) => - mapSlotsAsync(randomizeId(item), async (content) => - content.map(randomizeId) - ) - ) - ); - - return { - ...data, - props: { - ...data.props, - children, - }, - }; - }, - render: ({ children: Children }) => { - return ( -
- -
- ); - }, -}; - -export const Template = withLayout(TemplateInternal); +export * from "./client"; +export { type TemplateProps } from "./Template"; diff --git a/apps/demo/config/blocks/Template/server.tsx b/apps/demo/config/blocks/Template/server.tsx new file mode 100644 index 0000000000..cc3f915363 --- /dev/null +++ b/apps/demo/config/blocks/Template/server.tsx @@ -0,0 +1,9 @@ +import { ComponentConfig } from "@/core/types"; +import { withLayout } from "../../components/Layout"; +import TemplateComponent, { TemplateProps } from "./Template"; + +export const TemplateInternal: ComponentConfig = { + render: TemplateComponent, +}; + +export const Template = withLayout(TemplateInternal); diff --git a/apps/demo/config/components/Header/index.tsx b/apps/demo/config/components/Header/index.tsx index f3ff8c09e2..bb282dacaa 100644 --- a/apps/demo/config/components/Header/index.tsx +++ b/apps/demo/config/components/Header/index.tsx @@ -5,7 +5,10 @@ import styles from "./styles.module.css"; const getClassName = getClassNameFactory("Header", styles); const NavItem = ({ label, href }: { label: string; href: string }) => { - const navPath = window.location.pathname.replace("/edit", "") || "/"; + const navPath = + typeof window !== "undefined" + ? window.location.pathname.replace("/edit", "") || "/" + : "/"; const isActive = navPath === (href.replace("/edit", "") || "/"); diff --git a/apps/demo/config/components/Layout/index.tsx b/apps/demo/config/components/Layout/index.tsx index 7eab6ef58f..545e165a96 100644 --- a/apps/demo/config/components/Layout/index.tsx +++ b/apps/demo/config/components/Layout/index.tsx @@ -1,5 +1,9 @@ import { CSSProperties, forwardRef, ReactNode } from "react"; -import { ComponentConfig, DefaultComponentProps, ObjectField } from "@/core"; +import { + ComponentConfig, + DefaultComponentProps, + ObjectField, +} from "@/core/types"; import { spacingOptions } from "../../options"; import { getClassNameFactory } from "@/core/lib"; import styles from "./styles.module.css"; diff --git a/apps/demo/config/index.tsx b/apps/demo/config/index.tsx index eafffbe3c0..6767860a37 100644 --- a/apps/demo/config/index.tsx +++ b/apps/demo/config/index.tsx @@ -1,41 +1,18 @@ -import { Config, Data } from "@/core"; -import { Button, ButtonProps } from "./blocks/Button"; -import { Card, CardProps } from "./blocks/Card"; -import { Grid, GridProps } from "./blocks/Grid"; -import { Hero, HeroProps } from "./blocks/Hero"; -import { Heading, HeadingProps } from "./blocks/Heading"; -import { Flex, FlexProps } from "./blocks/Flex"; -import { Logos, LogosProps } from "./blocks/Logos"; -import { Stats, StatsProps } from "./blocks/Stats"; -import { Template, TemplateProps } from "./blocks/Template"; -import { Text, TextProps } from "./blocks/Text"; -import { Space, SpaceProps } from "./blocks/Space"; - -import Root, { RootProps } from "./root"; - -export type { RootProps } from "./root"; - -export type Props = { - Button: ButtonProps; - Card: CardProps; - Grid: GridProps; - Hero: HeroProps; - Heading: HeadingProps; - Flex: FlexProps; - Logos: LogosProps; - Stats: StatsProps; - Template: TemplateProps; - Text: TextProps; - Space: SpaceProps; -}; - -export type UserConfig = Config< - Props, - RootProps, - "layout" | "typography" | "interactive" ->; - -export type UserData = Data; +import { Button } from "./blocks/Button"; +import { Card } from "./blocks/Card"; +import { Grid } from "./blocks/Grid"; +import { Hero } from "./blocks/Hero"; +import { Heading } from "./blocks/Heading"; +import { Flex } from "./blocks/Flex"; +import { Logos } from "./blocks/Logos"; +import { Stats } from "./blocks/Stats"; +import { Template } from "./blocks/Template"; +import { Text } from "./blocks/Text"; +import { Space } from "./blocks/Space"; + +import Root from "./root"; +import { UserConfig } from "./types"; +import { initialData } from "./initial-data"; // We avoid the name config as next gets confused export const conf: UserConfig = { @@ -71,416 +48,6 @@ export const conf: UserConfig = { }, }; -export const initialData: Record = { - "/": { - content: [ - { - type: "Hero", - props: { - title: "This page was built with Puck", - description: - "Puck is the self-hosted visual editor for React. Bring your own components and make site changes instantly, without a deploy.", - buttons: [ - { - label: "Visit GitHub", - href: "https://github.com/measuredco/puck", - }, - { label: "Edit this page", href: "/edit", variant: "secondary" }, - ], - id: "Hero-1687283596554", - image: { - url: "https://images.unsplash.com/photo-1687204209659-3bded6aecd79?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2670&q=80", - mode: "inline", - }, - padding: "128px", - align: "left", - }, - readOnly: { title: false, description: false }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687298109536", - direction: "vertical", - }, - }, - { - type: "Heading", - props: { - align: "center", - level: "2", - text: "Drag-and-drop your own React components", - layout: { padding: "0px" }, - size: "xxl", - id: "Heading-1687297593514", - }, - }, - { - type: "Space", - props: { - size: "8px", - id: "Space-1687284122744", - direction: "vertical", - }, - }, - { - type: "Text", - props: { - align: "center", - text: "Configure Puck with your own components to make change for your marketing pages without a developer.", - layout: { padding: "0px" }, - size: "m", - id: "Text-1687297621556", - color: "muted", - }, - }, - { - type: "Space", - props: { - size: "40px", - id: "Space-1687296179388", - direction: "vertical", - }, - }, - { - type: "Grid", - props: { - id: "Grid-c4cd99ae-8c5e-4cdb-87d2-35a639f5163e", - gap: 24, - numColumns: 3, - items: [ - { - type: "Card", - props: { - title: "Built for content teams", - description: - "Puck enables content teams to make changes to their content without a developer or breaking the UI.", - icon: "pen-tool", - mode: "flat", - layout: { grow: true, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-66ab42c9-d1da-4c44-9dba-5d7d72f2178d", - }, - }, - { - type: "Card", - props: { - title: "Easy to integrate", - description: - "Front-end developers can easily integrate their own components using a familiar React API.", - icon: "git-merge", - mode: "flat", - layout: { grow: true, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-0012a293-8ef3-4e7c-9d7c-7da0a03d97ae", - }, - }, - { - type: "Card", - props: { - title: "No vendor lock-in", - description: - "Completely open-source, Puck is designed to be integrated into your existing React application.", - icon: "github", - mode: "flat", - layout: { grow: true, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-09efb3f3-f58d-4e07-a481-7238d7e57ad6", - }, - }, - ], - }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687287070296", - direction: "vertical", - }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687298110602", - direction: "vertical", - }, - }, - { - type: "Heading", - props: { - align: "center", - level: "2", - text: "The numbers", - layout: { padding: "0px" }, - size: "xxl", - id: "Heading-1687296574110", - }, - }, - { - type: "Space", - props: { - size: "16px", - id: "Space-1687284283005", - direction: "vertical", - }, - }, - { - type: "Text", - props: { - align: "center", - text: 'This page demonstrates Puck configured with a custom component library. This component is called "Stats", and contains some made-up numbers. You can configure any page by adding "/edit" onto the URL.', - layout: { padding: "0px" }, - size: "m", - id: "Text-1687284565722", - color: "muted", - maxWidth: "916px", - }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687297618253", - direction: "vertical", - }, - }, - { - type: "Stats", - props: { - items: [ - { title: "Users reached", description: "20M+" }, - { title: "Cost savings", description: "$1.5M" }, - { title: "Another stat", description: "5M kg" }, - { title: "Final fake stat", description: "15K" }, - ], - id: "Stats-1687297239724", - }, - }, - { - type: "Space", - props: { - size: "120px", - id: "Space-1687297589663", - direction: "vertical", - }, - }, - { - type: "Heading", - props: { - align: "center", - level: "2", - text: "Extending Puck", - layout: { padding: "0px" }, - size: "xxl", - id: "Heading-1687296184321", - }, - }, - { - type: "Space", - props: { - size: "8px", - id: "Space-1687296602860", - direction: "vertical", - }, - }, - { - type: "Text", - props: { - align: "center", - text: "Puck can also be extended with plugins and headless CMS content fields, transforming Puck into the perfect tool for your Content Ops.", - layout: { padding: "0px" }, - size: "m", - id: "Text-1687296579834", - color: "muted", - maxWidth: "916px", - }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687299311382", - direction: "vertical", - }, - }, - { - type: "Grid", - props: { - gap: 24, - numColumns: 3, - id: "Grid-2da28e88-7b7b-4152-9da0-9f93f41213b6", - items: [ - { - type: "Card", - props: { - title: "plugin-heading-analyzer", - description: - "Analyze the document structure and identify WCAG 2.1 issues with your heading hierarchy.", - icon: "align-left", - mode: "card", - layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-b0e8407d-9fbb-4e76-aa32-d32f655c11d3", - }, - }, - { - type: "Card", - props: { - title: "External data", - description: - "Connect your components with an existing data source, like Strapi.js.", - icon: "feather", - mode: "card", - layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-f8ebd568-3a30-4099-a068-22cabae4691b", - }, - }, - { - type: "Card", - props: { - title: "Custom plugins", - description: - "Create your own plugin to extend Puck for your use case using React.", - icon: "plug", - mode: "card", - layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-9c3b0acc-ee42-4a4a-8cc7-1b22d98493f1", - }, - }, - { - type: "Card", - props: { - title: "Title", - description: "Description", - icon: "Feather", - mode: "card", - layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-dbec4ae9-8208-49bf-8910-3347ff13d957", - }, - }, - { - type: "Card", - props: { - title: "Title", - description: "Description", - icon: "Feather", - mode: "card", - layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-e807464c-4974-4dbb-b1c9-989deabce58d", - }, - }, - { - type: "Card", - props: { - title: "Title", - description: "Description", - icon: "Feather", - mode: "card", - layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Card-3b4b7d53-2124-4d7a-a67e-36b24fd765b4", - }, - }, - ], - }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687299315421", - direction: "vertical", - }, - }, - { - type: "Heading", - props: { - align: "center", - level: "2", - text: "Get started", - layout: { padding: "0px" }, - size: "xxl", - id: "Heading-1687299303766", - }, - }, - { - type: "Space", - props: { - size: "16px", - id: "Space-1687299318902", - direction: "vertical", - }, - }, - { - type: "Text", - props: { - align: "center", - text: "Browse the Puck GitHub to get started, or try editing this page", - layout: { padding: "0px" }, - size: "m", - id: "Text-1687299305686", - color: "muted", - }, - }, - { - type: "Space", - props: { - size: "24px", - id: "Space-1687299335149", - direction: "vertical", - }, - }, - { - type: "Flex", - props: { - justifyContent: "center", - direction: "row", - gap: 24, - wrap: "wrap", - layout: { spanCol: 1, spanRow: 1, padding: "0px" }, - id: "Flex-7d63d5ff-bd42-4354-b05d-681b16436fd6", - items: [ - { - type: "Button", - props: { - label: "Visit GitHub", - href: "https://github.com/measuredco/puck", - variant: "primary", - id: "Button-bd41007c-6627-414d-839a-e261d470d8f9", - }, - }, - { - type: "Button", - props: { - label: "Edit this page", - href: "/edit", - variant: "secondary", - id: "Button-6a5fa26c-8a2d-4b08-a756-c46079877127", - }, - }, - ], - }, - }, - { - type: "Space", - props: { - size: "96px", - id: "Space-1687284290127", - direction: "vertical", - }, - }, - ], - root: { props: { title: "Puck Example" } }, - zones: {}, - }, - "/pricing": { - content: [], - root: { props: { title: "Pricing" } }, - }, - "/about": { - content: [], - root: { props: { title: "About Us" } }, - }, -}; - export const componentKey = Buffer.from( `${Object.keys(conf.components).join("-")}-${JSON.stringify(initialData)}` ).toString("base64"); diff --git a/apps/demo/config/initial-data.ts b/apps/demo/config/initial-data.ts new file mode 100644 index 0000000000..d4d465e281 --- /dev/null +++ b/apps/demo/config/initial-data.ts @@ -0,0 +1,411 @@ +import { UserData } from "./types"; + +export const initialData: Record = { + "/": { + content: [ + { + type: "Hero", + props: { + title: "This page was built with Puck", + description: + "Puck is the self-hosted visual editor for React. Bring your own components and make site changes instantly, without a deploy.", + buttons: [ + { + label: "Visit GitHub", + href: "https://github.com/measuredco/puck", + }, + { label: "Edit this page", href: "/edit", variant: "secondary" }, + ], + id: "Hero-1687283596554", + image: { + url: "https://images.unsplash.com/photo-1687204209659-3bded6aecd79?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2670&q=80", + mode: "inline", + }, + padding: "128px", + align: "left", + }, + readOnly: { title: false, description: false }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687298109536", + direction: "vertical", + }, + }, + { + type: "Heading", + props: { + align: "center", + level: "2", + text: "Drag-and-drop your own React components", + layout: { padding: "0px" }, + size: "xxl", + id: "Heading-1687297593514", + }, + }, + { + type: "Space", + props: { + size: "8px", + id: "Space-1687284122744", + direction: "vertical", + }, + }, + { + type: "Text", + props: { + align: "center", + text: "Configure Puck with your own components to make change for your marketing pages without a developer.", + layout: { padding: "0px" }, + size: "m", + id: "Text-1687297621556", + color: "muted", + }, + }, + { + type: "Space", + props: { + size: "40px", + id: "Space-1687296179388", + direction: "vertical", + }, + }, + { + type: "Grid", + props: { + id: "Grid-c4cd99ae-8c5e-4cdb-87d2-35a639f5163e", + gap: 24, + numColumns: 3, + items: [ + { + type: "Card", + props: { + title: "Built for content teams", + description: + "Puck enables content teams to make changes to their content without a developer or breaking the UI.", + icon: "pen-tool", + mode: "flat", + layout: { grow: true, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-66ab42c9-d1da-4c44-9dba-5d7d72f2178d", + }, + }, + { + type: "Card", + props: { + title: "Easy to integrate", + description: + "Front-end developers can easily integrate their own components using a familiar React API.", + icon: "git-merge", + mode: "flat", + layout: { grow: true, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-0012a293-8ef3-4e7c-9d7c-7da0a03d97ae", + }, + }, + { + type: "Card", + props: { + title: "No vendor lock-in", + description: + "Completely open-source, Puck is designed to be integrated into your existing React application.", + icon: "github", + mode: "flat", + layout: { grow: true, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-09efb3f3-f58d-4e07-a481-7238d7e57ad6", + }, + }, + ], + }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687287070296", + direction: "vertical", + }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687298110602", + direction: "vertical", + }, + }, + { + type: "Heading", + props: { + align: "center", + level: "2", + text: "The numbers", + layout: { padding: "0px" }, + size: "xxl", + id: "Heading-1687296574110", + }, + }, + { + type: "Space", + props: { + size: "16px", + id: "Space-1687284283005", + direction: "vertical", + }, + }, + { + type: "Text", + props: { + align: "center", + text: 'This page demonstrates Puck configured with a custom component library. This component is called "Stats", and contains some made-up numbers. You can configure any page by adding "/edit" onto the URL.', + layout: { padding: "0px" }, + size: "m", + id: "Text-1687284565722", + color: "muted", + maxWidth: "916px", + }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687297618253", + direction: "vertical", + }, + }, + { + type: "Stats", + props: { + items: [ + { title: "Users reached", description: "20M+" }, + { title: "Cost savings", description: "$1.5M" }, + { title: "Another stat", description: "5M kg" }, + { title: "Final fake stat", description: "15K" }, + ], + id: "Stats-1687297239724", + }, + }, + { + type: "Space", + props: { + size: "120px", + id: "Space-1687297589663", + direction: "vertical", + }, + }, + { + type: "Heading", + props: { + align: "center", + level: "2", + text: "Extending Puck", + layout: { padding: "0px" }, + size: "xxl", + id: "Heading-1687296184321", + }, + }, + { + type: "Space", + props: { + size: "8px", + id: "Space-1687296602860", + direction: "vertical", + }, + }, + { + type: "Text", + props: { + align: "center", + text: "Puck can also be extended with plugins and headless CMS content fields, transforming Puck into the perfect tool for your Content Ops.", + layout: { padding: "0px" }, + size: "m", + id: "Text-1687296579834", + color: "muted", + maxWidth: "916px", + }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687299311382", + direction: "vertical", + }, + }, + { + type: "Grid", + props: { + gap: 24, + numColumns: 3, + id: "Grid-2da28e88-7b7b-4152-9da0-9f93f41213b6", + items: [ + { + type: "Card", + props: { + title: "plugin-heading-analyzer", + description: + "Analyze the document structure and identify WCAG 2.1 issues with your heading hierarchy.", + icon: "align-left", + mode: "card", + layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-b0e8407d-9fbb-4e76-aa32-d32f655c11d3", + }, + }, + { + type: "Card", + props: { + title: "External data", + description: + "Connect your components with an existing data source, like Strapi.js.", + icon: "feather", + mode: "card", + layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-f8ebd568-3a30-4099-a068-22cabae4691b", + }, + }, + { + type: "Card", + props: { + title: "Custom plugins", + description: + "Create your own plugin to extend Puck for your use case using React.", + icon: "plug", + mode: "card", + layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-9c3b0acc-ee42-4a4a-8cc7-1b22d98493f1", + }, + }, + { + type: "Card", + props: { + title: "Title", + description: "Description", + icon: "Feather", + mode: "card", + layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-dbec4ae9-8208-49bf-8910-3347ff13d957", + }, + }, + { + type: "Card", + props: { + title: "Title", + description: "Description", + icon: "Feather", + mode: "card", + layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-e807464c-4974-4dbb-b1c9-989deabce58d", + }, + }, + { + type: "Card", + props: { + title: "Title", + description: "Description", + icon: "Feather", + mode: "card", + layout: { grow: false, spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Card-3b4b7d53-2124-4d7a-a67e-36b24fd765b4", + }, + }, + ], + }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687299315421", + direction: "vertical", + }, + }, + { + type: "Heading", + props: { + align: "center", + level: "2", + text: "Get started", + layout: { padding: "0px" }, + size: "xxl", + id: "Heading-1687299303766", + }, + }, + { + type: "Space", + props: { + size: "16px", + id: "Space-1687299318902", + direction: "vertical", + }, + }, + { + type: "Text", + props: { + align: "center", + text: "Browse the Puck GitHub to get started, or try editing this page", + layout: { padding: "0px" }, + size: "m", + id: "Text-1687299305686", + color: "muted", + }, + }, + { + type: "Space", + props: { + size: "24px", + id: "Space-1687299335149", + direction: "vertical", + }, + }, + { + type: "Flex", + props: { + justifyContent: "center", + direction: "row", + gap: 24, + wrap: "wrap", + layout: { spanCol: 1, spanRow: 1, padding: "0px" }, + id: "Flex-7d63d5ff-bd42-4354-b05d-681b16436fd6", + items: [ + { + type: "Button", + props: { + label: "Visit GitHub", + href: "https://github.com/measuredco/puck", + variant: "primary", + id: "Button-bd41007c-6627-414d-839a-e261d470d8f9", + }, + }, + { + type: "Button", + props: { + label: "Edit this page", + href: "/edit", + variant: "secondary", + id: "Button-6a5fa26c-8a2d-4b08-a756-c46079877127", + }, + }, + ], + }, + }, + { + type: "Space", + props: { + size: "96px", + id: "Space-1687284290127", + direction: "vertical", + }, + }, + ], + root: { props: { title: "Puck Example" } }, + zones: {}, + }, + "/pricing": { + content: [], + root: { props: { title: "Pricing" } }, + }, + "/about": { + content: [], + root: { props: { title: "About Us" } }, + }, +}; diff --git a/apps/demo/config/root.tsx b/apps/demo/config/root.tsx index 396806783a..8e84551c99 100644 --- a/apps/demo/config/root.tsx +++ b/apps/demo/config/root.tsx @@ -1,6 +1,6 @@ -import { DefaultRootProps, DropZone, RootConfig } from "@/core"; -import { Footer } from "./components/Footer"; +import { DefaultRootProps, RootConfig } from "@/core"; import { Header } from "./components/Header"; +import { Footer } from "./components/Footer"; export type RootProps = DefaultRootProps; @@ -8,13 +8,14 @@ export const Root: RootConfig = { defaultProps: { title: "My Page", }, - render: ({ puck }) => { + render: ({ puck: { isEditing, renderDropZone: DropZone } }) => { return (
-
+
+