Creating Your Movie Bookmark Application using Next js, Redux Toolkit, Firebase, and TypeScript
Tags: nextjs, react, typescript, redux
Published: 2023-08-31 22:46:53

In this tutorial, we'll demonstrate how to create a movie bookmark application using the Next.js framework (version 13.4.13). We'll also integrate the Redux Toolkit for efficient state management and data fetching, Firebase for storing movie data, and TypeScript for safe type parsing.

You can find the completed code on [GitHub](https://github.com/Tk-brainCodes/Movie-Bookmark), along with a [live](https://movie-bookmark.vercel.app/) demo. 

Below is a brief video clip demonstrating what we will be building.

{% vimeo 859871007 %}
<br/>
##Jump Ahead:
   * [Introduction to Next js](#nextjs)
   * [Prerequisite](#Prerequisite)
   * [Setting up a new Nextjs Project](#project)
   * [Installing packages](#packages)
   * [Project setup](#setup)
      * [Setting up the firebase config](#firebase)
      * [Understanding the folder structure](#structure)
   * [Setting up our redux store](#store)
      * [movie-bookmark/redux/store.ts](#store-ts)
      * [movie-bookmark/redux/hooks.ts](#hooks-ts)
   * [Setting up our redux slice](#slice)
      * [movie-bookmark/redux/features/bookmarkSlice.ts](#bookmark)
   * [Setting up our redux thunk](#bookmark-thunk)
      * [movie-bookmark/redux/features/bookmarkThunk.ts](#thunk)
   * [Making redux accessible in our application](#accessible)
      * [movie-bookmark/context/redux.provider.tsx](#accessible)
   * [Creating our components](#components)
      * [movie-bookmark/src/app/components/AnimationWrapper.tsx](#animate)
      * [movie-bookmark/src/app/components/Header.tsx](#header)
      * [movie-bookmark/src/app/components/MovieCard.tsx](#card)
   * [Creating our movies page](#movies)
      * [movie-bookmark/src/app/page.tsx](#movies-page)
   * [Creating our bookmarks page](#bookmark-p)
      * [movie-bookmark/src/app/bookmarks/page.tsx](#bookmark-page)
   * [Running our application](#running)
   * [Conclusion](#conclusion)

<br/>
<br/>
#Introducing Next js <a name="nextjs"></a>
Next.js is a frontend React framework that offers support for various rendering approaches, including:
* [Client-Side Component Rendering](https://nextjs.org/docs/app/building-your-application/rendering/client-components): In Next.js, client components can be utilized by including the `"use client"` directive at the beginning of the component file. These client components can be rendered during the request phase, eliminating the necessity of being retrieved from the server.
* [Server-Side Component Rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components): By default, Next.js uses server-side rendering, a technique that involves rendering and optional caching of UI components on the server. The cached results can be utilized multiple times without initiating new requests. This approach effectively enhances the optimization of your web application.

With the introduction of the 'App' directory in the early release of Next.js 13, the framework facilitates seamless [routing](https://nextjs.org/docs/app/building-your-application/routing/defining-routes) and [nested layout](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) structures between pages. Additionally, Next.js provides default [Search Engine Optimization (SEO)](https://nextjs.org/learn/seo/introduction-to-seo) features for your web applications.

<br/>
#Prerequisites <a name="Prerequisite"></a>
For this tutorial, you should have the following:

* Working knowledge of React and Typescript
* Your computer should have [Node.js](https://nodejs.org/en/download) v16 or a newer version installed.
* Make sure you have [Visual Studio Code](https://code.visualstudio.com/download) installed or any other preferred editor.

<br/>
#Setting up a new Next js project <a name="project"></a>
Creating a new Next.js application is made seamless with the utilization of the `create-next-app` command. This command takes care of setting up all the necessary components and configurations, simplifying the initial setup process. To begin, open your terminal and enter the following:

```shell
npx create-next-app@latest
```

You will be prompted to fill in the following inputs:

1. `What is your project named? movie-bookamrk`: let's name the project as **movie-bookmark**.
2. `Would you like to use TypeScript? No / Yes`: TypeScript is required for this project, so select **Yes**.
3. `Would you like to use ESLint? No / Yes`: ESLint is necessary for this project, so choose **Yes**.
4. `Would you like to use Tailwind CSS? No / Yes`: In this tutorial, we will utilize Tailwind CSS for styling, so choose **Yes**.
5. `Would you like to use src/ directory? No / Yes`: For the purpose of this project, we'll go with **Yes** as we'll be using the src directory.
6. `Would you like to use App Router? (recommended) No / Yes`: It's recommended to choose either No or Yes. In this case, choose **No**.
7. `Would you like to customize the default import alias? No / Yes`: Select **Yes** for default import alias.
8. `What import alias would you like configured? @/*`: press **Enter** to use the `@/*` alias.

Once you've completed these steps, in the terminal, navigate to the movie-bookmark directory and install the dependencies, then run the server:
```shell
# cd into directory
cd movie-bookmark

# install packages
npm install

# start server
npm run dev
```

<br/>
#Installing packages <a name="packages"></a>
In this tutorial, we will proceed with the installation of the necessary packages that are essential for our application. The following packages will be utilized, each accompanied by its respective installation command:


```shell
# framer for animations
npm i framer-motion

# firebase for database management
npm i firebase

# react-redux for state management
npm i react-redux

# for notification popups
npm i react-hot-toast

# for creating redux store, slice, and thunks 
npm i @reduxjs/toolkit

# for icons
npm i @mui/icons-material

# for effective styling of the material ui icons
npm i @emotion/styled @emotion/react
```

<br/>
#Project setup <a name="setup"></a>
####Setting up the firebase config <a name="firebase"></a>
Once you have successfully installed all the required packages, proceed to access your [Firebase console](https://firebase.google.com/?gad=1&gclid=EAIaIQobChMI9NbQwdv6gAMVUElBAh2MsgbKEAAYASAAEgKWLPD_BwE&gclsrc=aw.ds). Create a fresh Firebase project, and make sure to copy the configuration SDK provided by Firebase. Next, within the root directory of your project, create a file named `firebase.config.ts`. Paste the previously copied Firebase configuration into this file. Below, you will find the `firebase.config.ts` configuration file. Be sure to substitute the placeholder values with your actual configuration details. 

```Typescript
//firebase.config.ts

import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
import { getAuth } from "firebase/auth";

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_KEY,
  authDomain: process.env.NEXT_PUBLIC_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_MESSAGING_ID,
  appId: process.env.NEXT_PUBLIC_APP_ID,
  measurementId: process.env.NEXT_PUBLIC_MEASUREMENT_ID,
};

const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
export const auth = getAuth(app);

export default app;
```
The provided code demonstrates the setup of the Firebase app using the configuration stored in `firebaseConfig`. The `initializeApp` function produces an app instance, which serves as the gateway to Firebase services. To create instances of the Firestore and authentication services, use the `getFirestore` and `getAuth` functions. These functions configure the initially initialized Firebase app (app) with the respective instances.

####Understanding the folder strucutre <a name="structure"></a>
```
├── movie-bookmark
    ├── context
        ├── redux.provider.tsx
    ├── data
        ├── movie.ts
    ├── node_modules
    ├── public
    ├── redux
        ├── features
            ├── bookmarkSlice.ts
            ├── bookmarkThunk.ts
        ├── hooks.ts
        ├── store.ts
    ├── src/app
        ├── bookmarks
            ├── page.tsx
        ├── components
            ├── AnimationWrapper.tsx
            ├── Header.tsx
            ├── MovieCard.tsx
        ├── favicon.ico
        ├── global.css
        ├── layout.tsx
        ├── page.tsx
    ├── types
        ├── movie-types.ts
    ├── firebase.config.ts

```


In the folder structure above, we observe a `redux/` folder that houses the `features/` folder. Within the `features/` folder, we find the `bookmarkSlice.ts` and `bookmarkThunk.ts`, along with the `hooks.ts` and `store.ts` files. These files will be elaborated upon in the subsequent sections of this tutorial.

Moving on, the `src/app` directory takes a pivotal role in Next.js as it's where our routes are established. Notably, the `layout.ts` file serves as the foundational layout for our application, while the `page.tsx` file represents the primary page that a user sees when accessing `localhost:3000`.

In Next.js, each subfolder within the `src/app` directory signifies a distinct route segment. It's imperative that each of these subfolders contains an exported `page.tsx` file. Examining our current folder structure, we can identify the `bookmarks/` folder housing a `page.tsx` file, which corresponds to the route accessible at `localhost:3000/bookmarks`.

As we delve further, the `components/` folder is used for storing reusable components (for this project: `AnimationWrapper.tsx`, `Header.tsx`,and `MovieCard.tsx`). It's important to note that this folder doesn't serve as a route since it lacks a `page.tsx` file.
<br/>
#Setting up our redux store <a name="store"></a>
####movie-bookmark/redux/store.ts <a name="store-ts"></a>
Following the previously outlined folder structure, create a `redux/` folder, inside the folder create a `store.ts` file then add the following code:

```javascript
//store.ts

import { configureStore } from "@reduxjs/toolkit";

export const store = configureStore({
  reducer: {},
 middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
      devTools: process.env.NODE_ENV !== "production",
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```
In the above code snippet, we import `configureStore` to create a new Redux configuration called `store`. This `store` will hold our imported `reducers`. We adjust the middleware using `getDefaultMiddleware`, turning off `serializableCheck` for non-serializable state values. The `devTool` option enables the Redux dev tool only in development mode. `RootState` captures the complete store state using `store.getState()`'s return type. `AppDispatch` specifies the dispatch function's type for accurate action dispatching and minimal errors.

####movie-bookmark/redux/hooks.ts <a name="hooks-ts"></a>
Following the previously outlined folder structure, create a new file named `hooks.ts`inside the `redux/` folder and add the following code:

```javascript 
//hooks.ts

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
```
Frome the code above, instead of repeatedly importing the `RootState` and `AppDispatch` types into distinct components, it is preferable to build typed versions of the `useDispatch` and `useSelector` hooks. This approach improves the use of these hooks throughout your entire program.
<br/>
#Setting up our redux slice <a name="slice"></a>
####movie-bookmark/redux/features/bookmarkSlice.ts <a name="bookmark"></a>
Let's start by setting up our `bookmarkSlice`. To begin, create a `types/` declaration folder at the root directory, specifically for our `MovieCard` component type, `bookmark` and `user` state types. These types will later be imported into our `bookmarkSlice.ts` module.

Create a new file named `movie-type.ts` inside the `types/` folder, and insert the code provided below:

```javascript
//movie-type.ts

export interface UserProps {
  accessToken: string | any;
  auth: any;
  displayName: string;
  email: string | any;
  emailVerified: boolean;
  isAnonymous: boolean;
  metadata: any;
  phoneNumber: any;
  photoURL: string | any;
  proactiveRefresh: any;
  providerData: any;
  providerId: string | any;
  reloadListener: any | null;
  reloadUserInfo: any;
  tenantId: any | null;
  uid: string;
}

export interface MovieCardProps {
  title: string;
  movieId: number;
  poster_path: string;
  release_date: string;
  backdrop_path: string;
  id?: number;
  movieRating?: number;
  vote_average?: number;
  user: UserProps
}

export interface MovieThunkProp {
  background?: string;
  date?: string;
  poster_path: string;
  id: number;
  title: string;
}
```
In the provided code snippet, we are introducing three distinct types: `UserProps`, `MovieCardProps`, and `MovieThunkProps`. Each of these types has a specific purpose within our technical implementation, and their explanations are provided below.

The `UserProps` type pertains to the anticipated payload originating from Firebase authentication. This type describes the structure of the data we expect to receive from this process.

Furthermore, the `MovieCardProps` type is dedicated to our `MovieCard` component. This component will be thoroughly examined in the subsequent segment of this article. The `MovieCardProps` type outlines the expected structure of properties that can be utilized within the MovieCard component.

Lastly, the `MovieThunkProps` types come into play in the context of the bookmark state as well as interactions with the Firebase database. These types encompass the necessary specifications for handling state related to bookmarking and interfacing with the Firebase database.

Once you have declared the required types, proceed to create a new file named `bookmarkSlice.ts` within the `redux/features/` directory. Insert the following code:

```javascript
//bookmarkSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { MovieThunkProp, UserProps } from "../../types/movie.type";


interface BookMarkState {
  bookmarked: MovieThunkProp[];
  error: string | null;
  bookmarkError: string | null;
  user: UserProps | null;
}

const initialState: BookMarkState = {
  bookmarked: [],
  error: null,
  bookmarkError: null,
  user: null,
};

const bookmarkSlice = createSlice({
  name: "bookmark",
  initialState,
  reducers: {
    addMovieToBookmarked(state, action: PayloadAction<MovieThunkProp>) {
      state.bookmarked.unshift(action.payload);
    },
    removeFromBookmarked(state, action: PayloadAction<number>) {
      state.bookmarked = state.bookmarked.filter(
        (movie) => movie.id !== action.payload
      );
    },
    addBookmarkFail(state, action: PayloadAction<string>) {
      state.error = action.payload;
    },
    getBookmarkError(state, action: PayloadAction<string>) {
      state.bookmarkError = action.payload;
    },
    setUser(state, action) {
      state.user = action.payload;
    },
    updateBookmarks(state, action: PayloadAction<MovieThunkProp[]>) {
      state.bookmarked = action.payload;
    },
  },
});

export const {
  addMovieToBookmarked,
  removeFromBookmarked,
  addBookmarkFail,
  getBookmarkError,
  setUser,
  updateBookmarks,
} = bookmarkSlice.actions;

export default bookmarkSlice.reducer;
```
From the code above, we import the [createSlice](https://redux-toolkit.js.org/api/createSlice) and [PayloadAction](https://redux-toolkit.js.org/usage/usage-with-typescript) from redux toolkit, for creating our **bookmark slice**. We also import our `MovieThunKProp` and `UserProps` we created previously.

We then define our state, which holds the following:
* `bookmarked`: holds movie objects that are bookmarked.
* `error`: A string or null to hold any potential errors related to bookmark actions.
* `bookmarkError`: A string or null to hold errors related to bookmark operations.
* `user`: A UserProps object or null to store user information.

After defining our state, we then create our slice using the `createSlice` and name it `bookmark`, which holds the following reducer functions:

* `addMovieToBookmarked`: Adds a movie to the beginning of the bookmarked array.
* `removeFromBookmarked`: Removes a movie from the bookmarked array based on its ID.
* `addBookmarkFail`: Sets the error field with an error message while adding to the bookmark state.
* `getBookmarkError`: Sets the bookmarkError field with an error message while fetching from the database.
* `setUser`: Sets the user field with the payload.
* `updateBookmarks`: Updates the entire bookmarked array with the payload.

In order to enable Redux's access to our reducers – the components responsible for managing state updates – we need to ensure that our `redux/store.ts` file includes the appropriate import statement for `bookmarkReducer`. This particular file was generated in the preceding sections of this guide.:

```javascript
//store.ts

import { configureStore } from "@reduxjs/toolkit";
import bookmarkReducer from "./features/bookmarkSlice";

export const store = configureStore({
  reducer: {
    bookmark: bookmarkReducer,
  },
 middleware: getDefaultMiddleware =>
    getDefaultMiddleware({
      serializableCheck: false,
    }),
      devTools: process.env.NODE_ENV !== "production",
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```
<br/>
#Setting up our redux thunk <a name="bookmark-thunk"></a>
####movie-bookmark/redux/features/bookmarkThunk.ts <a name="thunk"></a>
Redux Thunk facilitates the asynchronous process of adding and removing movies from the Firebase bookmarks, as well as fetching movies from the Firebase database. 

Navigate to the `redux/features` directory and generate a new file named `bookmarkThunk.ts`. This file is responsible for handling user authentication, movie bookmarking, and fetching bookmarked movies using Firebase's Firestore and Authentication services. After creating the `bookmarkThunk.ts` file, proceed to insert the provided code below:

```javascript
//bookmarkThunk.ts

import { createAsyncThunk } from "@reduxjs/toolkit";
import { RootState } from "../store";
import { MovieThunkProp } from "../../types/movie.type";
import {
  setDoc,
  deleteDoc,
  collection,
  getDoc,
  doc,
  getDocs,
} from "firebase/firestore";
import { db } from "../../firebase.config";
import {
  addBookmarkFail,
  addMovieToBookmarked,
  getBookmarkError,
  removeFromBookmarked,
  updateBookmarks,
} from "./bookmarkSlice";
import { signOut, signInWithPopup, GoogleAuthProvider } from "firebase/auth";
import { auth } from "../../firebase.config";
import toast from "react-hot-toast";

export const notifySuccess = (message: string) => toast.success(message);
export const notifyError = (message: string) => toast.error(message);

//user logout
export const logout = createAsyncThunk("auth/logout", async () => {
  await signOut(auth);
});

//Google sign-in
export const googleSignIn = createAsyncThunk("auth/googleSignIn", async () => {
  const googleAuthProvider = new GoogleAuthProvider();
  await signInWithPopup(auth, googleAuthProvider);
});

//add movies bookmarks
export const addMovieToBookmarkedDB = createAsyncThunk(
  "bookmark/addMovieToBookmarked",
  async (movie: MovieThunkProp, { dispatch, getState }) => {
    const state = getState() as RootState;
    const user = state.bookmark.user;
    const movieId = movie.id.toString();
    const { background, date, id, poster_path, title } = movie;

    try {
      const bookmarkedItemRef = doc(db, `${user?.uid as string}`, movieId);
      const docSnap = await getDoc(bookmarkedItemRef);

      if (docSnap.exists()) {
        const existItem = docSnap.data();
        dispatch(
          addBookmarkFail(existItem.title + " already an existing item")
        );
      } else {
        notifySuccess(`adding ${title} to bookmarks`);
        await setDoc(doc(db, `${user?.uid as string}`, movieId), {
          background,
          date,
          id,
          poster_path,
          title,
        });
        notifySuccess(`${title} has been successfully added`);
        dispatch(addMovieToBookmarked(movie));
      }
    } catch (error: any) {
      notifyError(`failed to add  ${title}  ${error}`);
      dispatch(
        addBookmarkFail(
          error.response && error.response.data.message
            ? error.response.data.message
            : "Failed to add " + title + ": " + error.message
        )
      );
    }
  }
);

//remove movies from bookmarks
export const removeMovieFromBookmarks = createAsyncThunk(
  "bookmark/removeMovieFromBookmarks",
  async (id: number, { dispatch, getState }) => {
    const state = getState() as RootState;
    const user = state.bookmark.user;
    const movieId = id.toString();
    try {
      dispatch(removeFromBookmarked(id));
      await deleteDoc(doc(db, `${user?.uid as string}`, movieId));
      notifySuccess(`Movie Id: ${id} was successfully deleted`);
    } catch (error: any) {
      notifyError(`failed to remove  ${id}`);
      dispatch({
        type: "ADD_BOOKMARK_FAIL",
        payload:
          error.response && error.response.data.message
            ? error.response.data.message
            : error.message,
      });
    }
  }
);

//retrieve all bookmarked movies
export const getBookmarksFromFirebaseDB = createAsyncThunk(
  "bookmark/getBookmarksFromFirebaseDB",
  async (_, { getState, dispatch }) => {
    const state = getState() as RootState;
    const user = state.bookmark.user;
    const getBookmarkItems = async (db: any) => {
      const bookmarkCol = collection(db, `${user?.uid as string}`);
      const bookmarkSnapshot = await getDocs(bookmarkCol);
      const bookmarkList = bookmarkSnapshot.docs.map(
        (doc) => doc.data() as MovieThunkProp
      );
      return bookmarkList;
    };
    try {
      let allBookmarks = await getBookmarkItems(db);
      dispatch(updateBookmarks(allBookmarks));
    } catch (error: any) {
      dispatch(
        getBookmarkError(
          error.response && error.response.data.message
            ? error.response.data.message
            : error.message
        )
      );
    }
  }
);
```

The provided code snippet initiates with the import of essential modules, Redux state slices (`addBookmarkFail`, `addMovieToBookmarked`, `getBookmarkError`, `removeFromBookmarked`, and `updateBookmarks`), and required types.

Next, we define utility helper functions for notifications: `notifySuccess` and `notifyError`. These functions are responsible for presenting success and error messages to users, leveraging the `react-hot-toast` library.

Furthermore, we create the `signOut` thunk function, responsible for managing user logout. This function executes asynchronously upon dispatch and aligns with the "auth/logout" action.

We proceed by creating the `googleSignIn` thunk function, which oversees the Google sign-in procedure. It generates a `GoogleAuthProvider` instance and employs the `signInWithPopup` function to initialize the Google sign-in process.

Progressing onwards, the `addMovieToBookmarkedDB` thunk is established. This function facilitates the addition of movies to the user's bookmarked collection as `usersUid/movieId/movieData`. By deconstructing  `background, date, id, poster_path, title` from the `movie` object, it transmits the information to Firestore. Should the movie already be present in the bookmarks, an error action is dispatched. Alternatively, if the movie is successfully added to bookmarks, the success actions are dispatched.

Moving ahead, we construct the `removeMovieFromBookmarks` thunk, which manages the removal of movies from the user's bookmarked collection. This function interacts with Firestore using the movie's ID, effectively removing it from the collection. Upon successful removal, a notification indicating success is displayed;otherwise, if an issue arises, an error action is dispatched.

Lastly, the `getBookmarkFromDB` thunk is created to retrieve all bookmarked movies from the Firestore database. Subsequently, it updates the Redux state to reflect the changes.
<br/>
#Making redux accessible in our application <a name="accessible"></a>
####movie-bookmark/context/redux.provider.tsx
After successfully creating our Redux slice and thunk, the next step is to ensure that Redux is accessible throughout our entire application.

Navigate to the root directory of `movie-bookmark` and establish a new folder named `context`. Within this folder, create a file named `redux.provider.tsx`, and insert the provided code snippet:

```javascript
//redux.provider.tsx

"use client";

import { store } from "../redux/store";
import { Provider } from "react-redux";

export function Providers({ children }: { children: React.ReactNode }) {
  return <Provider store={store}>{children}</Provider>;
}
```
The code above shows the intergration of the Redux store into our Nextjs application using the `Provider` component.

From the code above, we utilize the `use-client` directive to classify this as a client-side component. We then import the `store` that was created in the earlier section of this article. The `Provider` component is employed, accepting a `children` prop that enables the rendering of nested components within the context of the Redux store.

After completing the previous step, navigate to the `src/app/layout.tsx` file to implement our Provider component within the context of our Next.js application.

```javascript
//layout.tsx

import "./globals.css";
import type { Metadata } from "next";
import { Montserrat } from "@next/font/google";
import { Inter } from "next/font/google";
import { Providers } from "../../context/redux.provider";

const monstserrat = Montserrat({
  weight: ["400", "700"],
  subsets: ["latin"],
  variable: "--font-monstserrat",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={`${monstserrat.className}  block items-center`}>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  );
}
```
In the provided code snippet, we've introduced the `Provider` component, which serves as a wrapper for our application's component structure. Additionally, we've set up the `Montserrat` font as our chosen font.
<br/>
#Creating our components<a name="components"></a>
####movie-bookmark/src/app/components/AnimationWrapper.tsx<a name="animate"></a>
Having made redux accessible accross our application, we'll dive into creating our component.

First we create the `AnimationWrapper.tsx` component which takes in a `children` props and create an initial `animate` and `exit` animations using the `framer-motion` library.

```javascript
//AnimationWrapper.tsx

import React, { ReactNode } from "react";
import { motion } from "framer-motion";

const animations = {
  initial: { opacity: 0, x: 10 },
  animate: { opacity: 1, x: 0 },
  exit: { opacity: 0, x: -10 },
};

const AnimatedWrapper = ({ children }: { children: ReactNode }) => {
  return (
    <motion.div
      variants={animations}
      initial='initial'
      animate='animate'
      exit='exit'
      transition={{ duration: 1 }}
    >
      {children}
    </motion.div>
  );
};
export default AnimatedWrapper;
```

####movie-bookmark/src/app/Header.tsx <a name="header"></a>
Moving forward, after creating our `AnimationWrapper.tsx` component, our next step is to develop the `Header.tsx` client-component. This component serves multiple purposes:

* It manages user authentication within the `useEffect` function, triggering a `setUser` action. 
* The user information is extracted from the `user` state using the `useAppSelector` hook and displayed appropriately.
* For seamless navigation to the `bookmark` page, a link is provided, utilizing the `usePathname` hook.
* Additionally, this component facilitates the dispatching of `logout` and `googleSignIn` thunks, effectively handling potential errors that may arise.
* To ensure up-to-date notification, the `getBookmarksFromFirebaseDB()` thunk is invoked. This action continually updates the count of bookmarked movies, which is then indicated as a badge on the bookmark icon.

```javascript
//Header.tsx

"use client";

import { useState, useEffect } from "react";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks";
import { googleSignIn } from "../../../redux/features/bookmarkThunk";
import { setUser } from "../../../redux/features/bookmarkSlice";
import { usePathname } from "next/navigation";
import { onAuthStateChanged } from "firebase/auth";
import {
  getBookmarksFromFirebaseDB,
  logout,
} from "../../../redux/features/bookmarkThunk";
import { auth } from "../../../firebase.config";
import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder";
import Image from "next/image";
import Link from "next/link";

const Header = () => {
  const pathname = usePathname();
  const dispatch = useAppDispatch();
  const user: any = useAppSelector((state) => state.bookmark.user);
  const bookmarkedMovies = useAppSelector((state) => state.bookmark.bookmarked);
  const [firebaseError, setFirebaseError] = useState<string>("");

  const handleGoogleSignIn = async () => {
    try {
      await dispatch(googleSignIn());
    } catch (error: any) {
      setFirebaseError(error.message);
    }
  };

  const handleLogout = () => {
    try {
      dispatch(logout());
    } catch (error: any) {
      setFirebaseError(error.message);
    }
  };

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (currentUser: any) => {
      dispatch(setUser(currentUser));
    });
    dispatch(getBookmarksFromFirebaseDB());

    return () => {
      unsubscribe();
    };
  }, [dispatch]);

  const length = bookmarkedMovies.length;

  return (
    <div className='flex px-4 py-4 items-center bg-black border-b-2 border-slate-600 justify-between'>
      {firebaseError && (
        <h3 className='text-red-600 bg-red-100 px-2 py-2 rounded-md text-sm w-[20em]'>
          {firebaseError}
        </h3>
      )}
      <Link href='/'>
        <h3 className='text-sm text-orange-600'>
          my<span className='text-white'>Bookmarks</span>
        </h3>
      </Link>
      <div className='flex gap-3'>
        <button
          className='px-3 py-2 bg-blue-600 text-sm text-white rounded-full'
          onClick={user ? handleLogout : handleGoogleSignIn}
        >
          {user ? "Sign out" : "Sign in"}
        </button>
        {user && (
          <Image
            src={user?.photoURL ? user?.photoURL : ""}
            alt={user?.email ? user?.email : ""}
            width={500}
            height={500}
            className='w-[30px] h-[30px] rounded-full text-white via-cyan-900 to-stone-500 bg-gradient-to-r max-sm:cursor-pointer'
            data-cy='user-profile-image'
            priority
          />
        )}
        <Link href='/bookmarks'>
          <button
            className={`${
              pathname === "/bookmarked"
                ? "text-orange-400 font-semibold"
                : "text-white"
            } block relative`}
            data-cy='bookmark-icon'
          >
            <span>
              <BookmarkBorderIcon />
            </span>
            {length && (
              <span className='absolute -top-[8px] -right-[10px] w-5 h-5 bg-red-600 rounded-full flex items-center font-normal justify-center text-white text-xs'>
                {user ? `${length}` : "0"}
              </span>
            )}
          </button>
        </Link>
      </div>
    </div>
  );
};
export default Header;
```
The Header component must be positioned at the top of our web application. To achieve this, navigate to the `src.app/layout.tsx` file and import the Header component. By placing this component before any child elements, you ensure its display at the top.

```javascript
//layout.tsx

import "./globals.css";
import type { Metadata } from "next";
import { Montserrat } from "@next/font/google";
import { Inter } from "next/font/google";
import { Providers } from "../../context/redux.provider";
import Header from "./components/Header";

const monstserrat = Montserrat({
  weight: ["400", "700"],
  subsets: ["latin"],
  variable: "--font-monstserrat",
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={`${monstserrat.className}  block items-center`}>
        <Providers>
          <Header />
          {children}
        </Providers>
      </body>
    </html>
  );
}

```

####movie-bookmark/src/app/MovieCard.tsx <a name="card"></a>
After successfully creating our `Header.tsx` component, let's proceed to the final component: the `MovieCard.tsx` component.

This component is designed to display a movie card that includes essential information such as the movie title, release date, poster image, rating (all of which are destructured into our component) and the ability to be bookmarked. The main functionalities of this component are as follows:

* **Bookmarking Feature:** The component integrates the `addMovieToBookmarkedDB()` and `removeMovieFromBookmarks()` thunks, which are responsible for managing the addition and removal of movies from the Firestore database when dispatched.

* **Existing Movie Check:** The component also performs a check to determine if a movie is already present in the bookmark collection using the `checkIfItemExists` function. It updates the existing state using the `setExists` function. This update occurs within the `useEffect` hook, which is triggered whenever an item is added to or removed from the Firestore database.

The user interface (UI) of the component showcases key elements such as the movie poster, title, rating, and release year. Additionally, a bookmark button is provided, allowing users to toggle between bookmarked and unbookmarked states for a particular movie. This dynamic behavior is facilitated by checking the existence of the movie in the Firestore database.

```javascript
//MovieCard.tsx

/* eslint-disable react-hooks/exhaustive-deps */
"use client";

import { useEffect, useState } from "react";
import { useAppSelector, useAppDispatch } from "../../../redux/hooks";
import {
  addMovieToBookmarkedDB,
  removeMovieFromBookmarks,
} from "../../../redux/features/bookmarkThunk";
import { MovieCardProps } from "../../../types/movie.type";
import { collection, getDocs } from "firebase/firestore";
import { db } from "../../../firebase.config";
import { Toaster } from "react-hot-toast";
import StarIcon from "@mui/icons-material/Star";
import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder";
import BookmarkIcon from "@mui/icons-material/Bookmark";
import Image from "next/image";

export default function MovieCard({
  title,
  poster_path,
  release_date,
  movieId,
  backdrop_path,
  movieRating,
  user,
}: MovieCardProps) {
  const imagePath = "https://image.tmdb.org/t/p/original";
  const [exists, setExists] = useState(false);
  const bookmarks = useAppSelector((state) => state.bookmark.bookmarked);
  const dispatch = useAppDispatch();

  const movieData = {
    background: backdrop_path,
    date: release_date,
    poster_path: poster_path,
    id: movieId,
    title: title,
  };

  const checkIfItemExists = async () => {
    const bookmarkCol = collection(db, `${user?.uid as string}`);
    const bookmarkSnapshot = await getDocs(bookmarkCol);
    const bookmarkList = bookmarkSnapshot.docs.map((doc) => doc.data());
    const itemExists = bookmarkList.some((item) => item.id === movieId);
    setExists(itemExists);
  };

  const handleAddToBookmark = (movie: any) => {
    try {
      if (user) {
        dispatch(addMovieToBookmarkedDB(movie));
      }
    } catch (error) {
      console.log(error);
    }
  };

  const handleRemoveMovieFromBookmark = (id: number) => {
    if (user) {
      dispatch(removeMovieFromBookmarks(id));
    }
  };

  useEffect(() => {
    checkIfItemExists();
  }, [exists, bookmarks]);

  return (
    <div className='w-fit mt-[20px]'>
      <Toaster />
      <div className='w-[250px]'>
        <Image
          src={imagePath + poster_path}
          alt={title || "movie"}
          className='h-[350px] w-[250px] max-sm:w-[350px] bg-stone-300 transition ease-in-out cursor-pointer hover:brightness-50'
          loading='lazy'
          width={500}
          height={500}
          blurDataURL={imagePath + backdrop_path}
          placeholder='blur'
        />
        <section className='flex items-center justify-between'>
          <div className='block'>
            <h1 className='mt-3 text-sm text-white font-semibold tracking-tight'>
              {title}
            </h1>
            <p className='text-sm flex gap-3 text-slate-400 font-normal mt-1'>
              <span>
                <StarIcon
                  style={{ fontSize: "16px" }}
                  className='text-orange-600'
                />
                {movieRating?.toFixed(1)}
              </span>
              <span>|</span>
              <span> {release_date?.substring(0, 4)}</span>
            </p>
          </div>
          <button
            className='px-3 py-2 text-blue-500 rounded-full hover:text-blue-400'
            onClick={() => {
              if (exists) {
                handleRemoveMovieFromBookmark(movieId);
              } else {
                handleAddToBookmark(movieData);
              }
            }}
          >
            {exists ? (
              <BookmarkIcon
                style={{ fontSize: "20px" }}
                className='text-blue-300  cursor-pointer'
              />
            ) : (
              <BookmarkBorderIcon
                style={{ fontSize: "20px" }}
                className='text-blue-300 cursor-pointer'
              />
            )}
          </button>
        </section>
      </div>
    </div>
  );
}
```
In the given code, you will notice that we are using an `imagePath` to display our TMDB images. To enable Next.js to access the domain endpoint, we'll configure the domains used in this tutorial within our `next.config.ts` file. This includes both the `TMDB` and `GOOGLE API` domains.

```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    domains: ['image.tmdb.org', 'lh3.googleusercontent.com']
  }
}

module.exports = nextConfig
```

<br/>
#Creating our movies page<a name="movies"></a>
####movie-bookmark/src/app/page.tsx<a name="movies-page"></a>
Before we start creating our movie page, we must provide it with the necessary movie data. In this tutorial, we will utilize static movie data acquired from the [TMDB API](https://developer.themoviedb.org/reference/intro/getting-started).

To get started, navigate to the root directory named `movie-bookmark`. Inside the `data` folder, go ahead and create a file called `movie.ts`. Once you've completed this step, insert the following static movie data:

```javascript
//data/movie.ts

export const results = [
  {
    adult: false,
    backdrop_path: "/znUYFf0Sez5lUmxPr3Cby7TVJ6c.jpg",
    id: 447277,
    title: "The Little Mermaid",
    original_language: "en",
    original_title: "The Little Mermaid",
    poster_path: "/ym1dxyOk4jFcSl4Q2zmRrA5BEEN.jpg",
    media_type: "movie",
    genre_ids: [12, 10751, 14, 10749],
    popularity: 2078.238,
    release_date: "2023-05-18",
    video: false,
    vote_average: 6.346,
    vote_count: 1015,
  },
  {
    adult: false,
    backdrop_path: "/ctMserH8g2SeOAnCw5gFjdQF8mo.jpg",
    id: 346698,
    title: "Barbie",
    original_language: "en",
    original_title: "Barbie",
    poster_path: "/iuFNMS8U5cb6xfzi51Dbkovj7vM.jpg",
    media_type: "movie",
    genre_ids: [35, 12, 14],
    popularity: 6058.224,
    release_date: "2023-07-19",
    video: false,
    vote_average: 7.631,
    vote_count: 1130,
  },
  {
    adult: false,
    backdrop_path: "/7drO1kYgQ0PnnU87sAnBEphYrSM.jpg",
    id: 1083862,
    title: "Resident Evil: Death Island",
    original_language: "ja",
    original_title: "バイオハザード：デスアイランド",
    poster_path: "/qayga07ICNDswm0cMJ8P3VwklFZ.jpg",
    media_type: "movie",
    genre_ids: [16, 28, 27],
    popularity: 813.767,
    release_date: "2023-06-22",
    video: false,
    vote_average: 8.347,
    vote_count: 160,
  },
  {
    adult: false,
    backdrop_path: "/fm6KqXpk3M2HVveHwCrBSSBaO0V.jpg",
    id: 872585,
    title: "Oppenheimer",
    original_language: "en",
    original_title: "Oppenheimer",
    poster_path: "/8Gxv8gSFCU0XGDykEGv7zR1n2ua.jpg",
    media_type: "movie",
    genre_ids: [18, 36],
    popularity: 1449.266,
    release_date: "2023-07-19",
    video: false,
    vote_average: 8.385,
    vote_count: 668,
  },
  {
    adult: false,
    backdrop_path: "/yF1eOkaYvwiORauRCPWznV9xVvi.jpg",
    id: 298618,
    title: "The Flash",
    original_language: "en",
    original_title: "The Flash",
    poster_path: "/rktDFPbfHfUbArZ6OOOKsXcv0Bm.jpg",
    media_type: "movie",
    genre_ids: [28, 12, 878],
    popularity: 5930.081,
    release_date: "2023-06-13",
    video: false,
    vote_average: 6.923,
    vote_count: 1603,
  },
  {
    adult: false,
    backdrop_path: "/pMCvRynXABgLBMKHYa2UXjTBMsU.jpg",
    id: 615,
    title: "Futurama",
    original_language: "en",
    original_name: "Futurama",
    poster_path: "/7RRHbCUtAsVmKI6FEMzZB6Re88P.jpg",
    media_type: "tv",
    genre_ids: [16, 35, 10765],
    popularity: 677.546,
    release_date: "1999-03-28",
    vote_average: 8.398,
    vote_count: 2755,
    origin_country: ["US"],
  },
  {
    adult: false,
    backdrop_path: "/sa9vB0xb3OMU6iSMkig8RBbdESq.jpg",
    id: 113962,
    title: "Special Ops: Lioness",
    original_language: "en",
    original_name: "Special Ops: Lioness",
    poster_path: "/rXCzevakJoAN1qnZY0nAQPSLVRv.jpg",
    media_type: "tv",
    genre_ids: [18],
    popularity: 270.057,
    release_date: "2023-07-23",
    vote_average: 8.2,
    vote_count: 41,
    origin_country: ["US"],
  },
  {
    adult: false,
    backdrop_path: "/2vFuG6bWGyQUzYS9d69E5l85nIz.jpg",
    id: 667538,
    title: "Transformers: Rise of the Beasts",
    original_language: "en",
    original_title: "Transformers: Rise of the Beasts",
    poster_path: "/gPbM0MK8CP8A174rmUwGsADNYKD.jpg",
    media_type: "movie",
    genre_ids: [28, 12, 878],
    popularity: 5458.192,
    release_date: "2023-06-06",
    video: false,
    vote_average: 7.469,
    vote_count: 1909,
  },
  {
    adult: false,
    backdrop_path: "/kIMYSzp1fH1H9adKplekLD9BuNi.jpg",
    id: 1003581,
    title: "Justice League: Warworld",
    original_language: "en",
    original_title: "Justice League: Warworld",
    poster_path: "/9tx4cD3cuHhrdqLwFw8TTbfSKH2.jpg",
    media_type: "movie",
    genre_ids: [16, 28, 878],
    popularity: 121.482,
    release_date: "2023-07-25",
    video: false,
    vote_average: 7.421,
    vote_count: 19,
  },
  {
    adult: false,
    backdrop_path: "/av2wp3R978lp1ZyCOHDHOh4FINM.jpg",
    id: 736769,
    title: "They Cloned Tyrone",
    original_language: "en",
    original_title: "They Cloned Tyrone",
    poster_path: "/hnzXoDaK346U4ByfvQenu2DZnTg.jpg",
    media_type: "movie",
    genre_ids: [35, 878, 9648],
    popularity: 128.81,
    release_date: "2023-06-14",
    video: false,
    vote_average: 7.022,
    vote_count: 115,
  },
];

```
After inserting the movie data, the movie page component functions as the primary webpage of the application and is reachable by navigating to the route `localhost:3000`. Within this component, our initial task is to substitute the default JSX code that is created by Next.js upon installation. Instead, we will integrate the subsequent code:

```javascript
"use client";

import { useEffect } from 'react';
import { results } from "../../data/movie";
import { useAppSelector } from "../../redux/hooks";
import MovieCard from "./components/MovieCard";
import AnimatedWrapper from "./components/AnimationWrapper";
import { getBookmarksFromFirebaseDB } from "../../redux/features/bookmarkThunk";
import { useAppDispatch } from '../../redux/hooks';


export default function Home() {
  const user: any = useAppSelector((state) => state.bookmark.user);
  const dispatch = useAppDispatch();


  useEffect(() => {
    dispatch(getBookmarksFromFirebaseDB());
  }, [dispatch])

  return (
    <>
      <div className='flex flex-col items-center justify-center h-auto px-4 py-4'>
        <AnimatedWrapper>
          <div className='grid grid-cols-4 max-md:grid-cols-2 gap-6  items-center max-sm:flex max-sm:justify-center max-sm:flex-col'>
            {results.map((movie) => {
              return (
                <div key={movie?.id}>
                  <MovieCard
                    title={movie?.title as string}
                    movieId={movie?.id as number}
                    poster_path={movie?.poster_path as string}
                    backdrop_path={movie?.backdrop_path as string}
                    release_date={movie?.release_date as string}
                    movieRating={movie?.vote_average as number}
                    user={user}
                  />
                </div>
              );
            })}
          </div>
        </AnimatedWrapper>
      </div>
    </>
  );
}
```

The provided code snippet involves importing various components and data. We first import the static data from `data/movie.ts`, which contains movie information. Additionally, we import the `MovieCard` component, previously designed for presenting movie details, and the `AnimationWrapper` component, responsible for adding a fading effect to our movie cards.

Next, we proceed to iterate through the movie data. During this iteration, we generate individual instances of the `MovieCard` for each movie. We ensure that pertinent details like title, IDs, paths, dates, ratings, and user information are appropriately passed on.

To maintain the accurate display of bookmarked movies present in Firestore, we employ the `getBookmarksFromDB` thunk. This thunk is dispatched and triggers updates whenever a re-render occurs. By employing this approach, we guarantee that movies existing in Firestore and marked as bookmarks are correctly displayed on the page.


#Creating our bookmarks page<a name="bookmark-p"></a>
####movie-bookmark/src/app/bookmarks/page.tsx <a name="bookmark-page"></a>
After completing the setup of our movies page, our next step involves establishing the bookmark page functionality. This page is responsible for retrieving bookmarks from the Firebase database and displaying them.

To proceed, navigate to the `src/app/bookmarks` directory. Inside this directory, create a new file named `page.tsx`. Once the file is created, insert the provided code:


```javascript
"use client";

import { useState, useEffect } from "react";
import {useAppSelector, useAppDispatch } from "../../../redux/hooks";
import {
  getBookmarksFromFirebaseDB,
  removeMovieFromBookmarks,
} from "../../../redux/features/bookmarkThunk";
import { Toaster } from "react-hot-toast";
import DeleteIcon from "@mui/icons-material/Delete";
import AnimatedWrapper from "../components/AnimationWrapper";
import Image from "next/image";


const Bookmark = () => {
  const imagePath = "https://image.tmdb.org/t/p/original";
  const bookmarkedMovies = useAppSelector((state) => state.bookmark.bookmarked);
  const dispatch = useAppDispatch();

  useEffect(() => {
    dispatch(getBookmarksFromFirebaseDB());
  }, [dispatch]);



  return (
    <AnimatedWrapper>
      <div className='flex flex-col items-center justify-center px-6 py-6 w-[100vw] overflow-x-hidden'>
        <Toaster />
        <h1 className='font-semibold mb-[20px] text-white'>My Bookmarks</h1>
        <div>
          {bookmarkedMovies?.length === 0 ? (
            <h2>Sorry no bookmarks :(</h2>
          ) : (
            <>
              <div className='grid grid-cols-4 max-md:grid-cols-2 gap-6 items-center max-sm:flex max-sm:justify-center max-sm:flex-col'>
                {bookmarkedMovies?.map((movie: any) => (
                  <div key={movie?.id} className='w-[250px]'>
                    <Image
                      src={imagePath + movie?.poster_path}
                      alt={`${movie?.title || ""}`}
                      className='h-[350px] w-[250px] bg-stone-300 transition ease-in-out cursor-pointer hover:brightness-50'
                      loading='lazy'
                      width={500}
                      height={500}
                      blurDataURL={imagePath + movie?.poster_path}
                      placeholder='blur'
                    />
                    <div className='flex gap-2 relative -mt-[20em] float-right px-2'>
                      <button
                        title='bookmark movie'
                        className={`text-xs bg-white text-slate-500 px-3 py-3 hover:scale-110 transition ease-in-out rounded-full`}
                        onClick={() =>
                          dispatch(removeMovieFromBookmarks(movie?.id))
                        }
                      >
                        <DeleteIcon className='text-red-500' />
                      </button>
                    </div>
                    <h1 className='mt-3 text-sm text-white font-semibold tracking-tight'>
                      {movie?.title}
                    </h1>
                    <p className='text-slate-400 font-normal mt-1'>
                      <span>{movie?.date?.substring(0, 4)}</span>
                    </p>
                  </div>
                ))}
              </div>
            </>
          )}
        </div>
      </div>
    </AnimatedWrapper>
  );
};

export default Bookmark;
``` 
In the provided code snippet, we retrieve the bookmarked movie from Firestore by executing the `getBookmarksFromFirebaseDB` thunk. This action updates the state of our `bookmarkMovies`. If users wish to remove movies from their bookmarks, this action is initiated through Redux, specifically using the `removeMovieFromBookmarks` action.

#Running our application <a name="running"></a>

Congratulations! You've made it to this point. To witness our application in action, follow these steps:

1. Open your command prompt.
2. Enter the following command to run the application:

```shell
npm run dev
```

You can view your application live by accessing at `http://localhost:3000`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0546tdcljamci2kf74dh.png)

<br/>
#Conclusion
This article guides you through the process of configuring and utilizing Redux Toolkit and Firebase within your Next.js 13.4 application. Furthermore, we illustrate the creation of a functional movie bookmark feature, showcasing the prowess of the Redux store in your Next.js project. By integrating these tools, you can proficiently handle your application's state and retrieve data from Firebase in an organized and streamlined manner.

I trust that you have found this tutorial beneficial. If you possess any feedback or questions, kindly share them in the comments section.