CoursesIntegrated Visual Editing with Next.jsAdd drag-and-drop elements
Track
Work-ready Next.js

Integrated Visual Editing with Next.js

Lesson
6

Add drag-and-drop elements

Log in to watch a video walkthrough of this lesson
Log in
Video thumbnail
Go beyond "click-to-edit" with additional affordances for rearranging arrays in your front end
Log in to mark your progress for each Lesson and Task
Update the post schema type fields to include an array of "related posts" to render at the bottom of your post type documents.
src/sanity/schemaTypes/postType.ts
export const postType = defineType({
// ...all other settings
fields: [
// ...all other fields
defineField({
name: "relatedPosts",
type: "array",
of: [{ type: "reference", to: { type: "post" } }],
}),
],
});
Update your single post query to return the array and resolve any references.
src/sanity/lib/queries.ts
export const POST_QUERY =
defineQuery(`*[_type == "post" && slug.current == $slug][0]{
_id,
title,
body,
mainImage,
publishedAt,
"categories": coalesce(
categories[]->{
_id,
slug,
title
},
[]
),
author->{
name,
image
},
relatedPosts[]{
_key, // required for drag and drop
...@->{_id, title, slug} // get fields from the referenced post
}
}`);
Update your types now that the GROQ query has changed.
npm run typegen
Create a new component to render the related Posts
src/components/RelatedPosts.tsx
// src/components/RelatedPosts.tsx
"use client";
import Link from "next/link";
import { createDataAttribute } from "next-sanity";
import { POST_QUERYResult } from "@/sanity/types";
import { client } from "@/sanity/lib/client";
import { useOptimistic } from "next-sanity/hooks";
const { projectId, dataset, stega } = client.config();
export const createDataAttributeConfig = {
projectId,
dataset,
baseUrl: typeof stega.studioUrl === "string" ? stega.studioUrl : "",
};
export function RelatedPosts({
relatedPosts,
documentId,
documentType,
}: {
relatedPosts: NonNullable<POST_QUERYResult>["relatedPosts"];
documentId: string;
documentType: string;
}) {
const posts = useOptimistic<
NonNullable<POST_QUERYResult>["relatedPosts"] | undefined,
NonNullable<POST_QUERYResult>
>(relatedPosts, (state, action) => {
if (action.id === documentId && action?.document?.relatedPosts) {
// Optimistic document only has _ref values, not resolved references
return action.document.relatedPosts.map(
(post) => state?.find((p) => p._key === post._key) ?? post
);
}
return state;
});
if (!posts) {
return null;
}
return (
<aside className="border-t">
<h2>Related Posts</h2>
<div className="not-prose text-balance">
<ul
className="flex flex-col sm:flex-row gap-0.5"
data-sanity={createDataAttribute({
...createDataAttributeConfig,
id: documentId,
type: documentType,
path: "relatedPosts",
}).toString()}
>
{posts.map((post) => (
<li
key={post._key}
className="p-4 bg-blue-50 sm:w-1/3 flex-shrink-0"
data-sanity={createDataAttribute({
...createDataAttributeConfig,
id: documentId,
type: documentType,
path: `relatedPosts[_key=="${post._key}"]`,
}).toString()}
>
<Link href={`/post/${post?.slug?.current}`}>{post.title}</Link>
</li>
))}
</ul>
</div>
</aside>
);
}

You will notice data-sanity attributes being added to the wrapping and individual tags of the list. As well as a useOptimistic hook to apply these changes in the UI immediately, while the mutation in the content lake is still happening.

Update the Post component to include the RelatedPosts component.
src/components/RelatedPosts.tsx
import { Author } from "@/components/Author";
import { Categories } from "@/components/Categories";
import { components } from "@/sanity/portableTextComponents";
import { PortableText } from "next-sanity";
import { POST_QUERYResult } from "@/sanity/types";
import { PublishedAt } from "@/components/PublishedAt";
import { Title } from "@/components/Title";
import { urlFor } from "@/sanity/lib/image";
import Image from "next/image";
import { RelatedPosts } from "@/components/RelatedPosts";
export function Post(props: NonNullable<POST_QUERYResult>) {
const {
_id,
title,
author,
mainImage,
body,
publishedAt,
categories,
relatedPosts,
} = props;
return (
<article className="grid lg:grid-cols-12 gap-y-12">
<header className="lg:col-span-12 flex flex-col gap-4 items-start">
<div className="flex gap-4 items-center">
<Categories categories={categories} />
<PublishedAt publishedAt={publishedAt} />
</div>
<Title>{title}</Title>
<Author author={author} />
</header>
{mainImage ? (
<figure className="lg:col-span-4 flex flex-col gap-2 items-start">
<Image
src={urlFor(mainImage).width(400).height(400).url()}
width={400}
height={400}
alt=""
/>
</figure>
) : null}
{body ? (
<div className="lg:col-span-7 lg:col-start-6 prose lg:prose-lg">
<PortableText value={body} components={components} />
<RelatedPosts
relatedPosts={relatedPosts}
documentId={_id}
documentType="post"
/>
</div>
) : null}
</article>
);
}

Add a few Related Posts to any post document. Now within Presentation, you should be able to drag-and-drop to reorder their position, and see the content change in the Studio.

You have 5 uncompleted tasks in this lesson
0 of 5