Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/demo/app/[...puckPath]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/demo/app/custom-ui/[...puckPath]/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
32 changes: 32 additions & 0 deletions apps/demo/app/rsc/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Metadata> {
return {
title: initialData["/"].root.title,
};
}

export default async function Page() {
const data = initialData["/"];
const metadata = {
example: "Hello, world",
};

const resolvedData = await resolveAllData<Props, RootProps>(
data,
conf,
metadata
);

return <Render config={conf} data={resolvedData} metadata={metadata} />;
}
97 changes: 97 additions & 0 deletions apps/demo/config/blocks/Hero/Hero.tsx
Original file line number Diff line number Diff line change
@@ -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<HeroProps> = ({
align,
title,
description,
buttons,
padding,
image,
puck,
}) => {
return (
<Section
className={getClassName({
left: align === "left",
center: align === "center",
hasImageBackground: image?.mode === "background",
})}
style={{ paddingTop: padding, paddingBottom: padding }}
>
{image?.mode === "background" && (
<>
<div
className={getClassName("image")}
style={{
backgroundImage: `url("${image?.url}")`,
}}
></div>

<div className={getClassName("imageOverlay")}></div>
</>
)}

<div className={getClassName("inner")}>
<div className={getClassName("content")}>
<h1>{title}</h1>
<p className={getClassName("subtitle")}>{description}</p>
<div className={getClassName("actions")}>
{buttons.map((button, i) => (
<Button
key={i}
href={button.href}
variant={button.variant}
size="large"
tabIndex={puck.isEditing ? -1 : undefined}
>
{button.label}
</Button>
))}
</div>
</div>

{align !== "center" && image?.mode !== "background" && image?.url && (
<div
style={{
backgroundImage: `url('${image?.url}')`,
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
borderRadius: 24,
height: 356,
marginLeft: "auto",
width: "100%",
}}
/>
)}
</div>
</Section>
);
};

export default Hero;
195 changes: 195 additions & 0 deletions apps/demo/config/blocks/Hero/client.tsx
Original file line number Diff line number Diff line change
@@ -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<HeroProps> = {
fields: {
quote: {
type: "external",
placeholder: "Select a quote",
showSearch: false,
renderFooter: ({ items }) => {
return (
<div>
{items.length} result{items.length === 1 ? "" : "s"}
</div>
);
},
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: <span>{item.description}</span>,
}),
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 }) => (
<FieldLabel
label={field.label || name}
readOnly={readOnly}
icon={<Link2 size="16" />}
>
<AutoField
field={{ type: "text" }}
value={value}
onChange={onChange}
readOnly={readOnly}
/>
</FieldLabel>
),
},
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,
};
Loading
Loading