24
loading...
This website collects cookies to deliver better user experience
type Post {
content: String!
published: Boolean
slug: String!
}
type Query {
allPosts: [Post!]
findPostBySlug(slug: String!): Post
}
mutation CreatePost {
createPost( data:{
content: "Hello World"
slug: "hello-world"
published: true
}){
content
published
slug
}
}
query {
findPostBySlug(slug: "hello-world"){
content
slug
published
}
}
query {
allPosts {
data {
content
published
slug
}
}
}
npx create-next-app fauna-blog
cd fauna-blog
npm i @apollo/client apollo-cache-inmemory apollo-client apollo-link-http @bomdi/codebox @editorjs/checklist @editorjs/delimiter @editorjs/editorjs @editorjs/header @editorjs/inline-code @editorjs/list @editorjs/marker @editorjs/paragraph @headlessui/react @heroicons/react @tailwindcss/forms editorjs-react-renderer graphql graphql-tag
import {
ApolloClient,
HttpLink,
ApolloLink,
InMemoryCache,
concat,
} from "@apollo/client";
const httpLink = new HttpLink({ uri: process.env.FAUNA_GRAPHQL_ENDPOINT });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization:
`Basic ${process.env.FAUNA_TOKEN}`,
},
}));
return forward(operation);
});
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: concat(authMiddleware, httpLink),
});
export default apolloClient;
.env
like the followingFAUNA_GRAPHQL_ENDPOINT="https://graphql.fauna.com/graphql"
FAUNA_TOKEN="YOUR-TOKEN"
Editor
. In this component ready
, the user makes some changes
, and when the user clicks on the save
button.
The last step is an important one for us because when the user clicks on the save button we want to send the result to Fauna Endpoint to save the blog post content.
import React from "react";
import { useEffect, useRef, useState } from "react";
import EditorJS from "@editorjs/editorjs";
import Header from "@editorjs/header";
import List from "@editorjs/list";
import Quote from "@editorjs/quote";
import Delimiter from "@editorjs/delimiter";
import InlineCode from "@editorjs/inline-code";
import Marker from "@editorjs/marker";
import Embed from "@editorjs/embed";
import Image from "@editorjs/image";
import Table from "@editorjs/table";
import Warning from "@editorjs/warning";
import Code from "@editorjs/code";
import Checklist from "@editorjs/checklist";
import LinkTool from "@editorjs/link";
import Raw from "@editorjs/raw";
import Paragraph from "@editorjs/paragraph";
import Codebox from "@bomdi/codebox";
import gql from "graphql-tag";
import apolloClient from "../lib/apolloClient";
export default function Editor() {
const editorRef = useRef(null);
const [editorData, setEditorData] = useState(null);
const initEditor = () => {
const editor = new EditorJS({
holderId: "editorjs",
tools: {
header: {
class: Header,
inlineToolbar: ["marker", "link"],
config: {
placeholder: 'Enter a header',
levels: [1, 2, 3, 4, 5, 6],
defaultLevel: 3
},
shortcut: "CMD+SHIFT+H",
},
image: Image,
code: Code,
paragraph: {
class: Paragraph,
inlineToolbar: true,
},
raw: Raw,
inlineCode: InlineCode,
list: {
class: List,
inlineToolbar: true,
shortcut: "CMD+SHIFT+L",
},
checklist: {
class: Checklist,
inlineToolbar: true,
},
quote: {
class: Quote,
inlineToolbar: true,
config: {
quotePlaceholder: "Enter a quote",
captionPlaceholder: "Quote's author",
},
shortcut: "CMD+SHIFT+O",
},
warning: Warning,
marker: {
class: Marker,
shortcut: "CMD+SHIFT+M",
},
delimiter: Delimiter,
inlineCode: {
class: InlineCode,
shortcut: "CMD+SHIFT+C",
},
linkTool: LinkTool,
embed: Embed,
codebox: Codebox,
table: {
class: Table,
inlineToolbar: true,
shortcut: "CMD+ALT+T",
},
},
// autofocus: true,
placeholder: "Write your story...",
data: {
blocks: [
{
type: "header",
data: {
text: "New blog post title here....",
level: 2,
},
},
{
type: "paragraph",
data: {
text: "Blog post introduction here....",
},
},
],
},
onReady: () => {
console.log("Editor.js is ready to work!");
editorRef.current = editor;
},
onChange: () => {
console.log("Content was changed");
},
onSave: () => {
console.log("Content was saved");
},
});
};
const handleSave = async () => {
// 1. GQL mutation to create a blog post in Fauna
const CREATE_POST = gql`
mutation CreatePost($content: String!, $slug: String!) {
createPost(data: {published: true, content: $content, slug: $slug}) {
content
slug
published
}
}
`;
// 2. Get the content from the editor
const outputData = await editorRef.current.save();
// 3. Get blog title to create a slug
for (let i = 0; i < outputData.blocks.length; i++) {
if (
outputData.blocks[i].type === "header" &&
outputData.blocks[i].data.level === 2
) {
var title = outputData.blocks[i].data.text;
break;
}
}
const slug = title.toLowerCase().replace(/ /g, "-").replace(/[^\w-]+/g, "");
//3. Pass the content to the mutation and create a new blog post
const { data } = await apolloClient.mutate({
mutation: CREATE_POST,
variables: {
content: JSON.stringify(outputData),
slug: slug,
},
});
};
useEffect(() => {
if(!editorRef.current) {
initEditor();
}
}, []);
return (
<div>
<div id="editorjs" />
<div className="flex justify-center -mt-30 mb-20">
<button
type="button"
onClick={handleSave}
className="inline-flex items-center px-12 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
>
Save
</button>
</div>
</div>
);
}
Index.js
is where it shows all the blog posts to the user when they land on our project. Like https://fauna-blog-psi.vercel.app
[slug].js
is a dynamic page where it shows a specific blog post content. Like https://fauna-blog-psi.vercel.app/posts/hello-world
new.js
is Where we can create a new blog post by using EditorJS. Like https://fauna-blog-psi.vercel.app/posts/new
getServerSideProps
function you can find the GraphQL function.import gql from "graphql-tag";
import apolloClient from "../lib/apolloClient";
import Link from "next/link";
export default function Home(posts) {
let allPosts = [];
posts.posts.map((post) => {
const content = JSON.parse(post.content);
const published = post.published;
const slug = post.slug;
for (let i = 0; i < content.blocks.length; i++) {
if (
content.blocks[i].type === "header" &&
content.blocks[i].data.level === 2
) {
var title = content.blocks[i].data.text;
break;
}
}
for (let i = 0; i < content.blocks.length; i++) {
if (content.blocks[i].type === "paragraph") {
var description = content.blocks[i].data.text;
break;
}
}
title === undefined ? (title = "Without Title") : (title = title);
description === undefined ? (description = "Without Description") : (description = description);
allPosts.push({
title,
description,
published,
slug,
});
});
return (
<div className="bg-white pt-12 pb-20 px-4 sm:px-6 lg:pt-12 lg:pb-28 lg:px-8">
<div className="relative max-w-lg mx-auto divide-y-2 divide-gray-200 lg:max-w-7xl">
<div>
<h2 className="text-3xl tracking-tight font-extrabold text-gray-900 sm:text-4xl">
From the blog
</h2>
<p className="mt-3 text-xl text-gray-500 sm:mt-4">
Don't miss these awesome posts with some of the best tricks and
hacks you'll find on the Internet!
</p>
</div>
<div className="mt-12 grid gap-16 pt-12 lg:grid-cols-3 lg:gap-x-5 lg:gap-y-12">
{allPosts.map((post) => (
<div
key={post.title}
className="border border-blue-100 py-8 px-6 rounded-md"
>
<div>
<Link href={`/posts/${post.slug}`}>
<a className="inline-block">
<span className="text-blue-100 bg-blue-800 inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium">
Article
</span>
</a>
</Link>
</div>
<Link href={`/posts/${post.slug}`}>
<a className="block mt-4">
<p className="text-xl font-semibold text-gray-900">
{post.title}
</p>
<p className="mt-3 text-base text-gray-500">
{post.description}
</p>
</a>
</Link>
<div className="mt-6 flex items-center">
<div className="flex-shrink-0">
<Link href={`/posts/${post.slug}`}>
<a>
<span className="sr-only">Paul York</span>
<img
className="h-10 w-10 rounded-full"
src="https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80"
alt=""
/>
</a>
</Link>
</div>
<div className="ml-3">
<p className="text-sm font-medium text-gray-900">
<span>Paul York</span>
</p>
<div className="flex space-x-1 text-sm text-gray-500">
<time dateTime="Nov 10, 2021">Nov 10, 2021</time>
<span aria-hidden="true">·</span>
<span>3 mins read</span>
</div>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
export async function getServerSideProps (context) {
// 1. GQL Queries to get Posts data from Faust
const POSTS_QUERY = gql`
query {
allPosts {
data {
content
published
slug
}
}
}
`;
const { data } = await apolloClient.query({
query: POSTS_QUERY,
});
return {
props: {
posts: data.allPosts.data,
},
};
}
import dynamic from "next/dynamic";
const Editor = dynamic(
() => import("../../components/editor"),
{ ssr: false }
);
export default function CreatePost() {
return (
<>
<div className="min-h-full">
<div className="bg-gray-800 pb-32">
<header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<h1 className="text-3xl font-bold text-white">
Create a new post
</h1>
</div>
</header>
</div>
<main className="-mt-32">
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
{/* Replace with your content */}
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="border-4 border-dashed border-gray-200 rounded-lg pt-10">
<Editor />
</div>
</div>
{/* /End replace */}
</div>
</main>
</div>
</>
);
}
findPostBySlug
query. Then we pass the blog data as ServerSideProps
. On this page, we use editorjs-react-renderer
to render the EditorJS output.import { useRouter } from "next/router";
import Output from "editorjs-react-renderer";
import gql from "graphql-tag";
import apolloClient from "../../lib/apolloClient";
import Link from "next/link";
export default function Post({ post }) {
const content = JSON.parse(post.content);
return (
<div className="min-h-full">
<div className="bg-gray-800 pb-32">
<header className="py-10">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<Link href="/">
<a className="text-3xl font-bold text-white">
Home
</a>
</Link>
</div>
</header>
</div>
<main className="-mt-32">
<div className="max-w-7xl mx-auto pb-12 px-4 sm:px-6 lg:px-8">
{/* Replace with your content */}
<div className="bg-white rounded-lg shadow px-5 py-6 sm:px-6">
<div className="border-4 border-dashed border-gray-200 rounded-lg py-10 px-32">
<Output data={content} />
</div>
</div>
{/* /End replace */}
</div>
</main>
</div>
);
}
export async function getServerSideProps(context) {
const { slug } = context.query;
const { data } = await apolloClient.query({
query: gql`
query Post($slug: String!) {
findPostBySlug(slug: $slug) {
content
published
slug
}
}
`,
variables: {
slug,
},
});
return {
props: {
post: data.findPostBySlug,
},
};
}