Previously, I have discussed BlockNote briefly. I have said enough that you guys know what it is and how to use it in our Next.js projects. But there’s a lot more ground I want to cover. And you can view this article as just one acre. Enough with the metaphors. Let’s get this bus on the road.
Previously on the Nifty Little Me blog…
I always wanted to start a section off like that. Anyway, BlockNote is a block-based rich text editor. It is built on top of ProseMirror and TipTap. BlockNote is also made for React. To get started with BlockNote, follow the documentation. Psst, it’s simple; don’t worry.
With that little summary out of the way, let’s start creating.
Setting up Firebase For Your Project
Create a project in Firebase, create a web app, and create a Firestore database. After you have done all of that, create two collections: a documents
and a folders
collection.
Documents collection:
content
createdAt
folder
title
Folders collection:
createdAt
name
Install firebase in your next.js project by running npm install firebase
. Inside your next.js project, create a firebase.ts file at the root. Add your config code inside of it:
import { initializeApp } from "firebase/app";
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: "FIREBASE_API_KEY",
authDomain: "AUTH_DOMAIN",
projectId: "FIREBASE_PROJECT_ID",
storageBucket: "STORAGE_BUCKET",
messagingSenderId: "MESSAGING_SENDER_ID",
appId: "APP_ID"
};
export const app = initializeApp(firebaseConfig);
const firestore = getFirestore(app);
export { firestore };
Now, we could jump into the folder and document functionality.
Creating Folders and Documents
In order to do this, you will need to create some folders and files. So, have a similar project structure to this:
project/
│
├──src/app/
| ├── Write/
│ | ├──[id]/
| │ | └──page.tsx
| | ├──folder/
│ | │ └──[id]/
│ │ │ │ └──page.tsx
│ ├── layout.tsx
| └── page.tsx
└──firebase.ts
Now, inside of your app/page.tsx file we need to do two things: add a way to create folders and documents and display the folders and documents created. We can achieve that with this code:
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { firestore } from "../../firebase";
import { collection, addDoc, getDocs, orderBy, query } from 'firebase/firestore';
interface Document {
id: string;
title: string;
folder: string;
content: string;
createdAt: Date;
}
interface Folder {
id: string;
name: string
createdAt: Date;
}
const Home: React.FC = () => {
const [documents, setDocuments] = useState<Document[]>([]);
const [folders, setFolders] = useState<Folder[]>([]);
const [title, setTitle] = useState('');
const [folderName, setName] = useState('');
useEffect(() => {
const fetchDocuments = async () => {
const docRef = collection(firestore, "documents");
const q = query(docRef, orderBy('createdAt', 'desc'));
const querySnapshot = await getDocs(q);
const docs = querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
})) as Document[];
setDocuments(docs);
};
const fetchFolders = async () => {
const docRef = collection(firestore, "folders");
const q = query(docRef, orderBy('createdAt', 'desc'));
const querySnapshot = await getDocs(q);
const docs = querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
})) as Folder[];
setFolders(docs);
};
fetchFolders();
fetchDocuments();
}, []);
const handleDocCreate = async () => {
if (!title) return;
try {
const docRef = await addDoc(collection(firestore, 'documents'), {
title: title,
folder: "",
content: "",
createdAt: new Date()
});
setDocuments(prevDocs => [
{ id: docRef.id, title: title, folder: "", content: "", createdAt: new Date() },
...prevDocs
]);
setTitle('');
} catch (error) {
console.error('Error adding document: ', error);
}
};
const handleFolderCreate = async () => {
if (!folderName) return;
try {
const docRef = await addDoc(collection(firestore, 'folders'), {
name: folderName,
createdAt: new Date()
});
setFolders(prevDocs => [
{ id: docRef.id, name: folderName, createdAt: new Date() },
...prevDocs
]);
setName('');
} catch (error) {
console.error('Error adding document: ', error);
}
};
return (
<main>
<div className="flex flex-row gap-2 p-4 bg-slate-500">
<div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter document title"
className="border border-gray-300 rounded p-2 mb-2 text-black"
/>
<button onClick={handleDocCreate}>Create Document</button>
</div>
<div>
<input
type="text"
value={folderName}
onChange={(e) => setName(e.target.value)}
placeholder="Enter Folder Name"
className="border border-gray-300 rounded p-2 mb-2 text-black"
/>
<button onClick={handleFolderCreate}>Create Folder</button>
</div>
</div>
<div>
<div>
<h2>Documents List</h2>
<ul className="flex flex-col gap-2">
{documents.map(doc => (
<li key={doc.id}>
<Link href={`/write/${doc.id}?id=${doc.id}`}>
<h1 className="cursor-pointer hover:underline underline-offset-2 text-2xl">{doc.title}</h1>
</Link>
</li>
))}
</ul>
</div>
<hr/>
<div>
<h2>Folders</h2>
<ul className="flex flex-col gap-2">
{folders.map(doc => (
<li key={doc.id}>
<Link href={`/write/folder/${doc.id}?id=${doc.id}`}>
<h1 className="cursor-pointer hover:underline underline-offset-2 text-2xl">{doc.name}</h1>
</Link>
</li>
))}
</ul>
</div>
</div>
</main>
);
};
export default Home;
In the folder/[id] page.tsx
file, let’s add a way to create documents inside of the folder, fetch folder details, fetch folder documents, and display the documents using this code:
'use client';
import React, { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import Link from "next/link";
import { firestore } from '../../../.././../firebase';
import { getDoc, doc, where, getDocs, query, collection, addDoc } from 'firebase/firestore';
interface Document {
id: string;
title: string;
folder: string;
content: string;
createdAt: Date;
}
function Folder() {
const params = useSearchParams();
const folderId = params.get("id");
const [folderName, setName] = useState('');
const [documents, setDocuments] = useState<Document[]>([]);
const [title, setTitle] = useState('');
const handleDocCreate = async () => {
if (!title) return;
if (!folderId) return;
try {
const docRef = await addDoc(collection(firestore, 'documents'), {
title: title,
folder: folderId,
content: "",
createdAt: new Date()
});
setDocuments(prevDocs => [
{ id: docRef.id, title: title, folder: folderId, content: "", createdAt: new Date() },
...prevDocs
]);
setTitle('');
} catch (error) {
console.error('Error adding document: ', error);
}
};
useEffect(() => {
const fetchFolderDetails = async () => {
if (!folderId) return;
const docRef = doc(firestore, `folders/${folderId}`);
try {
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const data = docSnap.data();
setName(data.name || '');
} else {
console.log('Document does not exist');
}
} catch (error) {
console.error('Error fetching document: ', error);
}
};
const fetchFolderDocuments = async () => {
if (!folderId) return;
const docRef = collection(firestore, "documents");
const q = query(docRef, where('folder', '==', folderId));
const querySnapshot = await getDocs(q);
const docs = querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
})) as Document[];
setDocuments(docs);
};
fetchFolderDocuments();
fetchFolderDetails();
}, [folderId]);
return (
<div>
<div className="flex flex-row gap-4">
<h1>{folderName}</h1>
<div>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter document title"
className="border border-gray-300 rounded p-2 mb-2 text-black"
/>
<button onClick={handleDocCreate}>Create Document</button>
</div>
</div>
<h2>Documents</h2>
<ul>
{documents.map((doc) => (
<li key={doc.id}>
<Link href={`/write/${doc.id}?id=${doc.id}`}>
{doc.title}
</Link>
</li>
))}
</ul>
</div>
);
}
export default Folder;
Lastly, in our write/[id] page.tsx
file, we need to fetch the document’s title and content if it has content, parse HTML to blocks, add a way to save the document, and display the title, editor, and save button. We could accomplish that with this code:
'use client';
import React, { useState, useEffect, useRef, ChangeEvent, useCallback } from 'react';
import { useSearchParams } from 'next/navigation';
import { firestore } from '../../../../../firebase';
import { getDoc, doc, updateDoc } from 'firebase/firestore';
import "@blocknote/core/fonts/inter.css";
import { useCreateBlockNote } from "@blocknote/react";
import { BlockNoteView } from "@blocknote/mantine";
import "@blocknote/mantine/style.css";
import { Block } from "@blocknote/core";
function Document() {
const params = useSearchParams();
const docId = params.get("id");
const [title, setTitle] = useState('');
const [value, setValue] = useState('');
const [blocks, setBlocks] = useState<Block[]>([]);
useEffect(() => {
const fetchDocument = async () => {
if (!docId) return;
const docRef = doc(firestore, `documents/${docId}`);
try {
const docSnap = await getDoc(docRef);
if (docSnap.exists()) {
const data = docSnap.data();
setTitle(data.title || '');
setValue(data.content || '');
} else {
console.log('Document does not exist');
}
} catch (error) {
console.error('Error fetching document: ', error);
}
};
fetchDocument();
}, [docId]);
const editor = useCreateBlockNote();
useEffect(() => {
async function loadInitialHTML() {
const blocks = await editor.tryParseHTMLToBlocks(value);
editor.replaceBlocks(editor.document, blocks);
}
loadInitialHTML();
}, [editor, value]);
const saveDocument = async () => {
if (!docId) return;
const content = await editor.blocksToHTMLLossy(blocks);
try {
await updateDoc(doc(firestore, `documents/${docId}`), {
content: content,
});
console.log('Document saved successfully');
} catch (error) {
console.error('Error saving document: ', error);
}
};
return (
<div>
<h1>{title}</h1>
<BlockNoteView editor={editor} onChange={() => { setBlocks(editor.document); }} />
<button onClick={saveDocument}>Save Document</button>
</div>
);
}
export default Document;
Once you do all of that, you have an application that creates folders and documents and stores them in firebase. So, that wraps up this article. If you want to be updated every time I publish subscribe to my newsletter. Also, follow me on Medium.
Happy Coding!
Top comments (0)