Blog app with Nextjs (app router), React, MUI framework
July 19th 2024

Setting up Firebase Firestore

 

First, ensure you have set up Firebase in your Next.js project. Install the necessary dependencies:

 

npm install firebase
npm install @mui/material @emotion/react @emotion/styled

Create a firebase.js file to initialize Firebase:

// firebase.js
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_AUTH_DOMAIN",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_STORAGE_BUCKET",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID",
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

export { db };

 

Creating the Blog Component

 

Create a Blog component to display individual blog posts:

// components/Blog.js
import React from "react";
import { Card, CardContent, Typography, Chip } from "@mui/material";

const Blog = ({ blog }) => {
  return (
    <Card>
      <CardContent>
        <Typography variant="h5" component="div">
          {blog.title}
        </Typography>
        <Typography variant="body2" color="text.secondary">
          {blog.content}
        </Typography>
        <div>
          {blog.tags.map((tag) => (
            <Chip key={tag} label={tag} />
          ))}
        </div>
      </CardContent>
    </Card>
  );
};

export default Blog;

Fetching Blogs with Pagination and Filtering

 

Create a utility function to fetch blogs from Firestore:

// utils/fetchBlogs.js
import { collection, query, where, orderBy, limit, startAfter, getDocs } from "firebase/firestore";
import { db } from "../firebase";

export const fetchBlogs = async (lastVisible = null, tag = null, pageSize = 5) => {
  let q = collection(db, "blogs");

  if (tag) {
    q = query(q, where("tags", "array-contains", tag));
  }

  q = query(q, orderBy("createdAt"), limit(pageSize));

  if (lastVisible) {
    q = query(q, startAfter(lastVisible));
  }

  const snapshot = await getDocs(q);
  const blogs = snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }));
  const lastVisibleDoc = snapshot.docs[snapshot.docs.length - 1];

  return { blogs, lastVisible: lastVisibleDoc };
};

Creating the Main Blog Page

Create the main blog page to display the list of blogs with pagination and tag filtering:

// pages/blogs/index.js
import React, { useState, useEffect } from "react";
import { fetchBlogs } from "../../utils/fetchBlogs";
import Blog from "../../components/Blog";
import { Button, Container, Grid, TextField, Typography } from "@mui/material";

const BlogsPage = () => {
  const [blogs, setBlogs] = useState([]);
  const [lastVisible, setLastVisible] = useState(null);
  const [tag, setTag] = useState("");
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const loadBlogs = async () => {
      setLoading(true);
      const { blogs, lastVisible } = await fetchBlogs();
      setBlogs(blogs);
      setLastVisible(lastVisible);
      setLoading(false);
    };

    loadBlogs();
  }, []);

  const loadMoreBlogs = async () => {
    setLoading(true);
    const { blogs: newBlogs, lastVisible: newLastVisible } = await fetchBlogs(lastVisible, tag);
    setBlogs([...blogs, ...newBlogs]);
    setLastVisible(newLastVisible);
    setLoading(false);
  };

  const handleFilterChange = (e) => {
    setTag(e.target.value);
  };

  const handleFilterApply = async () => {
    setLoading(true);
    const { blogs, lastVisible } = await fetchBlogs(null, tag);
    setBlogs(blogs);
    setLastVisible(lastVisible);
    setLoading(false);
  };

  return (
    <Container>
      <Typography variant="h4" gutterBottom>
        Blogs
      </Typography>
      <TextField
        label="Filter by tag"
        variant="outlined"
        value={tag}
        onChange={handleFilterChange}
        fullWidth
        margin="normal"
      />
      <Button variant="contained" color="primary" onClick={handleFilterApply} disabled={loading}>
        Apply Filter
      </Button>
      <Grid container spacing={2}>
        {blogs.map((blog) => (
          <Grid item xs={12} sm={6} md={4} key={blog.id}>
            <Blog blog={blog} />
          </Grid>
        ))}
      </Grid>
      {lastVisible && (
        <Button variant="contained" color="secondary" onClick={loadMoreBlogs} disabled={loading}>
          Load More
        </Button>
      )}
    </Container>
  );
};

export default BlogsPage;

Adding Firestore Data Security Rules

Ensure you have appropriate security rules in Firestore to allow read access:

service cloud.firestore {
  match /databases/{database}/documents {
    match /blogs/{blog} {
      allow read: if true;
    }
  }
}

Conclusion

This code provides a basic implementation of a blog app with Next.js, Firebase Firestore, and Material-UI. The main features include fetching blogs, filtering by tags, and supporting pagination. Make sure to adjust the Firebase configuration and security rules according to your project’s needs.

 

Huy Le
Huy Le
[email protected]

Full-stack developer passionate about React and react native, firebase

Tags
next
app router
react
firebase
Social Share
Loading...