Skip to content

Commit

Permalink
Add Drag & Drop for Visual Editing (#86)
Browse files Browse the repository at this point in the history
* Add Drag & Drop for Visual Editing

* Update types and improve a few a couple small things surrounding page building

* Update next-sanity
  • Loading branch information
kenjonespizza authored Dec 2, 2024
1 parent d2a82e5 commit 649ca7a
Show file tree
Hide file tree
Showing 14 changed files with 487 additions and 20,147 deletions.
35 changes: 27 additions & 8 deletions nextjs-app/app/components/BlockRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ import React from "react";

import Cta from "@/app/components/Cta";
import Info from "@/app/components/InfoSection";
import { dataAttr } from "@/sanity/lib/utils";

type BlocksType = {
[key: string]: React.FC<any>;
};

type BlockType = {
_type: string;
_id: string;
_key: string;
};

type BlockProps = {
index: number;
block: BlockType;
pageId: string;
pageType: string;
};

const Blocks: BlocksType = {
Expand All @@ -25,14 +28,30 @@ const Blocks: BlocksType = {
/**
* Used by the <PageBuilder>, this component renders a the component that matches the block type.
*/
export default function BlockRenderer({ block, index }: BlockProps) {
export default function BlockRenderer({
block,
index,
pageId,
pageType,
}: BlockProps) {
// Block does exist
if (typeof Blocks[block._type] !== "undefined") {
return React.createElement(Blocks[block._type], {
key: block._id,
block: block,
index: index,
});
return (
<div
key={block._key}
data-sanity={dataAttr({
id: pageId,
type: pageType,
path: `pageBuilder[_key=="${block._key}"]`,
}).toString()}
>
{React.createElement(Blocks[block._type], {
key: block._key,
block: block,
index: index,
})}
</div>
);
}
// Block doesn't exist yet
return React.createElement(
Expand All @@ -41,6 +60,6 @@ export default function BlockRenderer({ block, index }: BlockProps) {
A &ldquo;{block._type}&rdquo; block hasn&apos;t been created
</div>
),
{ key: block._id }
{ key: block._key }
);
}
119 changes: 88 additions & 31 deletions nextjs-app/app/components/PageBuilder.tsx
Original file line number Diff line number Diff line change
@@ -1,48 +1,105 @@
"use client";

import { SanityDocument } from "next-sanity";
import { useOptimistic } from "next-sanity/hooks";
import Link from "next/link";

import BlockRenderer from "@/app/components/BlockRenderer";
import { Page } from "@/sanity.types";
import { dataAttr } from "@/sanity/lib/utils";
import { studioUrl } from "@/sanity/lib/api";

type PageBuilderPageProps = {
page: Page;
};

type PageBuilderSection = {
_key: string;
_type: string;
};

type PageData = {
_id: string;
_type: string;
pageBuilder?: PageBuilderSection[];
};

/**
* The PageBuilder component is used to render the blocks from the `pageBuilder` field in the Page type in your Sanity Studio.
*/
export default function PageBuilder({ page }: PageBuilderPageProps) {
if (page?.pageBuilder && page.pageBuilder.length > 0) {
return (
<>
{page.pageBuilder.map((block: any, index: number) => (
<BlockRenderer key={block._key} index={index} block={block} />
))}
</>
);
}

// If there are no blocks in the page builder.

function renderSections(pageBuilderSections: PageBuilderSection[], page: Page) {
return (
<div
data-sanity={dataAttr({
id: page._id,
type: page._type,
path: `pageBuilder`,
}).toString()}
>
{pageBuilderSections.map((block: any, index: number) => (
<BlockRenderer
key={block._key}
index={index}
block={block}
pageId={page._id}
pageType={page._type}
/>
))}
</div>
);
}

function renderEmptyState(page: Page) {
return (
<>
<div className="container">
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
This page has no content!
</h1>
<p className="mt-2 text-base text-gray-500">
Open the page in Sanity Studio to add content.
</p>
<div className="mt-10 flex">
<Link
className="rounded-full flex gap-2 mr-6 items-center bg-black hover:bg-red-500 focus:bg-cyan-500 py-3 px-6 text-white transition-colors duration-200"
href={`${studioUrl}/structure/intent/edit/template=page;type=page;path=pageBuilder;id=${page._id}`}
target="_blank"
rel="noopener noreferrer"
>
Add content to this page
</Link>
</div>
<div className="container">
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
This page has no content!
</h1>
<p className="mt-2 text-base text-gray-500">
Open the page in Sanity Studio to add content.
</p>
<div className="mt-10 flex">
<Link
className="rounded-full flex gap-2 mr-6 items-center bg-black hover:bg-red-500 focus:bg-cyan-500 py-3 px-6 text-white transition-colors duration-200"
href={`${studioUrl}/structure/intent/edit/template=page;type=page;path=pageBuilder;id=${page._id}`}
target="_blank"
rel="noopener noreferrer"
>
Add content to this page
</Link>
</div>
</>
</div>
);
}

export default function PageBuilder({ page }: PageBuilderPageProps) {
const pageBuilderSections = useOptimistic<
PageBuilderSection[] | undefined,
SanityDocument<PageData>
>(page?.pageBuilder, (currentSections, action) => {
// The action contains updated document data from Sanity
// when someone makes an edit in the Studio

// If the edit was to a different document, ignore it
if (action.id !== page._id) {
return currentSections;
}

// If there are sections in the updated document, use them
if (action.document.pageBuilder) {
// Reconcile References. https://www.sanity.io/docs/enabling-drag-and-drop#ffe728eea8c1
return action.document.pageBuilder.map(
(section) =>
currentSections?.find((s) => s._key === section?._key) || section
);
}

// Otherwise keep the current sections
return currentSections;
});

return pageBuilderSections && pageBuilderSections.length > 0
? renderSections(pageBuilderSections, page)
: renderEmptyState(page);
}
Loading

0 comments on commit 649ca7a

Please sign in to comment.