Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/visual editing dnd #87

Merged
merged 6 commits into from
Dec 2, 2024
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The Studio integrates with Sanity's Content Lake, offering hosted content APIs w
- **Next.js 15, Fast and Performant:** Static site built with Next.js App Router for excellent speed and SEO.
- **Real-time Visual Editing:** Use Sanity's [Presentation](https://www.sanity.io/docs/presentation) tools to see live updates as you edit.
- **Live Content:** The [Live Content API](https://www.sanity.io/live) allows you to deliver live, dynamic experiences to your users without the complexity and scalability challenges that typically come with building real-time functionality.
- **Customizable Pages:** Create and manage pages using a page builder with dynamic components.
- **Customizable Pages with Drag-and-Drop:** Create and manage pages using a page builder with dynamic components and [Drag-and-Drop Visual Editing](https://www.sanity.io/visual-editing-for-structured-content).
- **Powerful Content Management:** Collaborate with team members in real-time, with fine-grained revision history.
- **AI-powered Media Support:** Auto-generate alt text with [Sanity AI Assist](https://www.sanity.io/ai-assist).
- **On-demand Publishing:** No waiting for rebuilds—new content is live instantly with Incremental Static Revalidation.
Expand Down
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
Loading