DEV Community

It's Just Nifty
It's Just Nifty

Posted on • Originally published at niftylittleme.com on

Using Firebase To Store Folders and BlockNote Documents In Next.Js

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.

Screenshot of vscode

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 };
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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)