Skip to content

Commit 649ca7a

Browse files
Add Drag & Drop for Visual Editing (#86)
* Add Drag & Drop for Visual Editing * Update types and improve a few a couple small things surrounding page building * Update next-sanity
1 parent d2a82e5 commit 649ca7a

14 files changed

+487
-20147
lines changed

nextjs-app/app/components/BlockRenderer.tsx

+27-8
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,22 @@ import React from "react";
22

33
import Cta from "@/app/components/Cta";
44
import Info from "@/app/components/InfoSection";
5+
import { dataAttr } from "@/sanity/lib/utils";
56

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

1011
type BlockType = {
1112
_type: string;
12-
_id: string;
13+
_key: string;
1314
};
1415

1516
type BlockProps = {
1617
index: number;
1718
block: BlockType;
19+
pageId: string;
20+
pageType: string;
1821
};
1922

2023
const Blocks: BlocksType = {
@@ -25,14 +28,30 @@ const Blocks: BlocksType = {
2528
/**
2629
* Used by the <PageBuilder>, this component renders a the component that matches the block type.
2730
*/
28-
export default function BlockRenderer({ block, index }: BlockProps) {
31+
export default function BlockRenderer({
32+
block,
33+
index,
34+
pageId,
35+
pageType,
36+
}: BlockProps) {
2937
// Block does exist
3038
if (typeof Blocks[block._type] !== "undefined") {
31-
return React.createElement(Blocks[block._type], {
32-
key: block._id,
33-
block: block,
34-
index: index,
35-
});
39+
return (
40+
<div
41+
key={block._key}
42+
data-sanity={dataAttr({
43+
id: pageId,
44+
type: pageType,
45+
path: `pageBuilder[_key=="${block._key}"]`,
46+
}).toString()}
47+
>
48+
{React.createElement(Blocks[block._type], {
49+
key: block._key,
50+
block: block,
51+
index: index,
52+
})}
53+
</div>
54+
);
3655
}
3756
// Block doesn't exist yet
3857
return React.createElement(
@@ -41,6 +60,6 @@ export default function BlockRenderer({ block, index }: BlockProps) {
4160
A &ldquo;{block._type}&rdquo; block hasn&apos;t been created
4261
</div>
4362
),
44-
{ key: block._id }
63+
{ key: block._key }
4564
);
4665
}
+88-31
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,105 @@
1+
"use client";
2+
3+
import { SanityDocument } from "next-sanity";
4+
import { useOptimistic } from "next-sanity/hooks";
15
import Link from "next/link";
26

37
import BlockRenderer from "@/app/components/BlockRenderer";
48
import { Page } from "@/sanity.types";
9+
import { dataAttr } from "@/sanity/lib/utils";
510
import { studioUrl } from "@/sanity/lib/api";
611

712
type PageBuilderPageProps = {
813
page: Page;
914
};
1015

16+
type PageBuilderSection = {
17+
_key: string;
18+
_type: string;
19+
};
20+
21+
type PageData = {
22+
_id: string;
23+
_type: string;
24+
pageBuilder?: PageBuilderSection[];
25+
};
26+
1127
/**
1228
* The PageBuilder component is used to render the blocks from the `pageBuilder` field in the Page type in your Sanity Studio.
1329
*/
14-
export default function PageBuilder({ page }: PageBuilderPageProps) {
15-
if (page?.pageBuilder && page.pageBuilder.length > 0) {
16-
return (
17-
<>
18-
{page.pageBuilder.map((block: any, index: number) => (
19-
<BlockRenderer key={block._key} index={index} block={block} />
20-
))}
21-
</>
22-
);
23-
}
24-
25-
// If there are no blocks in the page builder.
30+
31+
function renderSections(pageBuilderSections: PageBuilderSection[], page: Page) {
32+
return (
33+
<div
34+
data-sanity={dataAttr({
35+
id: page._id,
36+
type: page._type,
37+
path: `pageBuilder`,
38+
}).toString()}
39+
>
40+
{pageBuilderSections.map((block: any, index: number) => (
41+
<BlockRenderer
42+
key={block._key}
43+
index={index}
44+
block={block}
45+
pageId={page._id}
46+
pageType={page._type}
47+
/>
48+
))}
49+
</div>
50+
);
51+
}
52+
53+
function renderEmptyState(page: Page) {
2654
return (
27-
<>
28-
<div className="container">
29-
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
30-
This page has no content!
31-
</h1>
32-
<p className="mt-2 text-base text-gray-500">
33-
Open the page in Sanity Studio to add content.
34-
</p>
35-
<div className="mt-10 flex">
36-
<Link
37-
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"
38-
href={`${studioUrl}/structure/intent/edit/template=page;type=page;path=pageBuilder;id=${page._id}`}
39-
target="_blank"
40-
rel="noopener noreferrer"
41-
>
42-
Add content to this page
43-
</Link>
44-
</div>
55+
<div className="container">
56+
<h1 className="text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl">
57+
This page has no content!
58+
</h1>
59+
<p className="mt-2 text-base text-gray-500">
60+
Open the page in Sanity Studio to add content.
61+
</p>
62+
<div className="mt-10 flex">
63+
<Link
64+
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"
65+
href={`${studioUrl}/structure/intent/edit/template=page;type=page;path=pageBuilder;id=${page._id}`}
66+
target="_blank"
67+
rel="noopener noreferrer"
68+
>
69+
Add content to this page
70+
</Link>
4571
</div>
46-
</>
72+
</div>
4773
);
4874
}
75+
76+
export default function PageBuilder({ page }: PageBuilderPageProps) {
77+
const pageBuilderSections = useOptimistic<
78+
PageBuilderSection[] | undefined,
79+
SanityDocument<PageData>
80+
>(page?.pageBuilder, (currentSections, action) => {
81+
// The action contains updated document data from Sanity
82+
// when someone makes an edit in the Studio
83+
84+
// If the edit was to a different document, ignore it
85+
if (action.id !== page._id) {
86+
return currentSections;
87+
}
88+
89+
// If there are sections in the updated document, use them
90+
if (action.document.pageBuilder) {
91+
// Reconcile References. https://www.sanity.io/docs/enabling-drag-and-drop#ffe728eea8c1
92+
return action.document.pageBuilder.map(
93+
(section) =>
94+
currentSections?.find((s) => s._key === section?._key) || section
95+
);
96+
}
97+
98+
// Otherwise keep the current sections
99+
return currentSections;
100+
});
101+
102+
return pageBuilderSections && pageBuilderSections.length > 0
103+
? renderSections(pageBuilderSections, page)
104+
: renderEmptyState(page);
105+
}

0 commit comments

Comments
 (0)