Integrate Firebase with a Next.js app

1. Before you begin

In this codelab, you'll learn how to integrate Firebase with a Next.js web app called Friendly Eats, which is a website for restaurant reviews.

Friendly Eats web app

The completed web app offers useful features that demonstrate how Firebase can help you build Next.js apps. These features include the following:

  • Automatic build and deploy: This codelab uses Firebase App Hosting to automatically build and deploy your Next.js code every time you push to a configured branch.
  • Sign-in and sign-out: The completed web app lets you sign in with Google and sign out. User login and persistence is managed entirely through Firebase Authentication.
  • Images: The completed web app lets signed-in users upload restaurant images. Image assets are stored in Cloud Storage for Firebase. The Firebase JavaScript SDK provides a public URL to uploaded images. This public URL is then stored in the relevant restaurant document in Cloud Firestore.
  • Reviews: The completed web app lets signed-in users post reviews of restaurants that consist of a star rating and a text-based message. Review information is stored in Cloud Firestore.
  • Filters: The completed web app lets signed-in users filter the list of restaurants based on category, location, and price. You can also customize the sorting method used. Data is accessed from Cloud Firestore, and Firestore queries are applied based on the filters used.

Prerequisites

  • A GitHub account
  • Knowledge of Next.js and JavaScript

What you'll learn

  • How to use Firebase with the Next.js App Router and server-side rendering.
  • How to persist images in Cloud Storage for Firebase.
  • How to read and write data in a Cloud Firestore database.
  • How to use sign-in with Google with the Firebase JavaScript SDK.

What you'll need

  • Git
  • A recent stable version of Node.js
  • A browser of your choice, such as Google Chrome
  • A development environment with a code editor and terminal
  • A Google account for the creation and management of your Firebase project
  • The ability to upgrade your Firebase project to the Blaze pricing plan

2. Set up your development environment and GitHub repository

This codelab provides the app's starter codebase and relies on the Firebase CLI.

Create a GitHub repository

The codelab source can be found at https://github.com/firebase/friendlyeats-web. The repository contains sample projects for multiple platforms. However, this codelab uses only the nextjs-start directory. Take note of the following directories:

* `nextjs-start`: contains the starter code upon which you build.
* `nextjs-end`: contains the solution code for the finished web app.

Copy the nextjs-start folder into your own repository:

  1. Using a terminal, create a new folder on your computer and change into the new directory:
    mkdir codelab-friendlyeats-web
    
    cd codelab-friendlyeats-web
    
  2. Use the giget npm package to fetch only the nextjs-start folder:
    npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
    
  3. Track changes locally with git:
    git init
    
    git commit -a -m "codelab starting point"
    
    git branch -M main
    
  4. Create a new GitHub repository: https://github.com/new. Name it anything you'd like.
    1. GitHub will give you a new repository URL that looks like either https://github.com//.git or git@github.com:/.git. Copy this URL.
  5. Push local changes to your new GitHub repository. Run the following command, substituting your repository URL for the placeholder.
    git remote add origin <your-repository-url>
    
    git push -u origin main
    
  6. You should now see the starter code in your GitHub repository.

Install or update the Firebase CLI

Run the following command to verify that you have the Firebase CLI installed and that it's v13.9.0 or higher:

firebase --version

If you see a lower version or you don't have the Firebase CLI installed, run the install command:

npm install -g firebase-tools@latest

If you're unable to install the Firebase CLI because of permission errors, see the npm documentation or use another installation option.

Log in to Firebase

  1. Run the following command to log in to the Firebase CLI:
    firebase login
    
  2. Depending on whether you want Firebase to collect data, enter Y or N.
  3. In your browser, select your Google account, and then click Allow.

3. Set up your Firebase project

In this section, you'll set up a Firebase project and associate a Firebase web app with it. You'll also set up the Firebase services used by the sample web app.

Create a Firebase project

  1. In the Firebase console, click Add project.
  2. In the Enter your project name text box, enter FriendlyEats Codelab (or a project name of your choice), and then click Continue.
  3. In the Confirm Firebase billing plan modal, confirm the plan is Blaze, and then click Confirm plan
  4. For this codelab, you don't need Google Analytics, so toggle off the Enable Google Analytics for this project option.
  5. Click Create project.
  6. Wait for your project to provision, and then click Continue.
  7. In your Firebase project, go to Project Settings. Note your project ID because you need it later. This unique identifier is how your project is identified (for example, in the Firebase CLI).

Upgrade your Firebase pricing plan

To use Firebase App Hosting and Cloud Storage for Firebase, your Firebase project needs to be on the pay-as-you go (Blaze) pricing plan, which means it's linked to a Cloud Billing account.

  • A Cloud Billing account requires a payment method, like a credit card.
  • If you're new to Firebase and Google Cloud, check if you're eligible for a $300 credit and a Free Trial Cloud Billing account.
  • If you're doing this codelab as part of an event, ask your organizer if there are any Cloud credits available.

To upgrade your project to the Blaze plan, follow these steps:

  1. In the Firebase console, select to upgrade your plan.
  2. Select the Blaze plan. Follow the on-screen instructions to link a Cloud Billing account to your project.
    If you needed to create a Cloud Billing account as part of this upgrade, you might need to navigate back to the upgrade flow in the Firebase console to complete the upgrade.

Add a web app to your Firebase project

  1. Navigate to your Project overview in your Firebase project, and then click e41f2efdd9539c31.png Web.

    If you already have apps registered in your project, click Add app to see the Web icon.
  2. In the App nickname text box, enter a memorable app nickname, such as My Next.js app.
  3. Keep the Also set up Firebase Hosting for this app checkbox unchecked.
  4. Click Register app > Next > Next > Continue to console.

Set up Firebase services in the Firebase console

Set up Authentication

  1. In the Firebase console, navigate to Authentication.
  2. Click Get started.
  3. In the Additional providers column, click Google > Enable.
  4. In the Public-facing name for project text box, enter a memorable name, such as My Next.js app.
  5. From the Support email for project drop-down, select your email address.
  6. Click Save.

Set up Cloud Firestore

  1. In the left-panel of the Firebase console, expand Build and then select Firestore database.
  2. Click Create database.
  3. Leave the Database ID set to (default).
  4. Select a location for your database, then click Next.
    For a real app, you want to choose a location that's close to your users.
  5. Click Start in test mode. Read the disclaimer about the security rules.
    Later in this codelab, you'll add Security Rules to secure your data. Do not distribute or expose an app publicly without adding Security Rules for your database.
  6. Click Create.

Set up Cloud Storage for Firebase

  1. In the left-panel of the Firebase console, expand Build and then select Storage.
  2. Click Get started.
  3. Select a location for your default Storage bucket.
    Buckets in US-WEST1, US-CENTRAL1, and US-EAST1 can take advantage of the "Always Free" tier for Google Cloud Storage. Buckets in all other locations follow Google Cloud Storage pricing and usage.
  4. Click Start in test mode. Read the disclaimer about the security rules.
    Later in this codelab, you'll add security rules to secure your data. Do not distribute or expose an app publicly without adding Security Rules for your Storage bucket.
  5. Click Create.

4. Review the starter codebase

In this section, you'll review a few areas of the app's starter codebase to which you'll add functionality in this codelab.

Folder and file structure

The following table contains an overview of the folder and file structure of the app:

Folders and files

Description

src/components

React components for filters, headers, restaurant details, and reviews

src/lib

Utility functions that aren't necessarily bound to React or Next.js

src/lib/firebase

Firebase-specific code and Firebase configuration

public

Static assets in the web app, like icons

src/app

Routing with the Next.js App Router

src/app/restaurant

An API route handler

package.json and package-lock.json

Project dependencies with npm

next.config.js

Next.js-specific configuration (server actions are enabled)

jsconfig.json

JavaScript language-service configuration

Server and client components

The app is a Next.js web app that uses the App Router. Server rendering is used throughout the app. For example, the src/app/page.js file is a server component responsible for the main page. The src/components/RestaurantListings.jsx file is a client component denoted by the "use client" directive at the beginning of the file.

Import statements

You might notice import statements like the following:

import RatingPicker from "@/src/components/RatingPicker.jsx";

The app uses the @ symbol to avoid clunky relative import paths and is made possible by path aliases.

Firebase-specific APIs

All Firebase API code is wrapped in the src/lib/firebase directory. Individual React components then import the wrapped functions from the src/lib/firebase directory, rather than importing Firebase functions directly.

Mock data

Mock restaurant and review data is contained in the src/lib/randomData.js file. Data from that file is assembled in the code in the src/lib/fakeRestaurants.js file.

5. Create an App Hosting backend

In this section, you'll set up an App Hosting backend to watch a branch on your git repository.

By the end of this section, you'll have an App Hosting backend connected to your repository in GitHub that will automatically re-build and roll out a new version of your app whenever you push a new commit to your main branch.

Deploy Security Rules

The code already has sets of security rules for Firestore and for Cloud Storage for Firebase. After you deploy the Security Rules, the data in your database and your bucket are better protected from misuse.

  1. In your terminal, configure the CLI to use the Firebase project you created earlier:
    firebase use --add
    
    When prompted for an alias, enter friendlyeats-codelab.
  2. To deploy these Security Rules, run this command in your terminal:
    firebase deploy --only firestore:rules,storage
    
  3. If you're asked: "Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?", press Enter to select Yes.

Add your Firebase configuration to your web app code

  1. In the Firebase console, navigate to your Project settings.
  2. In the SDK setup and configuration pane, click "Add app" and click on the code brackets icon to register a new web app.
  3. At the end of the web app creation flow, copy the firebaseConfig variable, and copy its properties and their values.
  4. Open the apphosting.yaml file in your code editor, and fill in the environment variable values with the configuration values from the Firebase console.
  5. In the file, replace the existing properties with those that you copied.
  6. Save the file.

Create a backend

  1. Navigate to the App Hosting page in the Firebase console:

The zero state of the App Hosting console, with a 'Get Started' button

  1. Click "Get started" to start the backend creation flow. Configure your backend as follows:
  2. Follow the prompts in the first step to connect the GitHub repository you created earlier.
  3. Set deployment settings:
    1. Keep the root directory as /
    2. Set the live branch to main
    3. Enable automatic rollouts
  4. Name your backend friendlyeats-codelab.
  5. In "Create or associate a Firebase web app", pick the web app you configured earlier from the "Select an existing Firebase web app" drop down.
  6. Click "Finish and deploy". After a moment, you'll be taken to a new page where you can see the status of your new App Hosting backend!
  7. Once your rollout completes, click your free domain under "domains". This may take a few minutes to begin working due to DNS propagation.

You've deployed the initial web app! Every time you push a new commit to the main branch of your GitHub repository, you'll see a new build and rollout begin in the Firebase console, and your site will automatically update once the rollout completes.

6. Add authentication to the web app

In this section, you add authentication to the web app so that you can log in to it.

Implement the sign-in and sign-out functions

  1. In the src/lib/firebase/auth.js file, replace the onAuthStateChanged, signInWithGoogle, and signOut functions with the following code:
export function onAuthStateChanged(cb) {
	return _onAuthStateChanged(auth, cb);
}

export async function signInWithGoogle() {
  const provider = new GoogleAuthProvider();

  try {
    await signInWithPopup(auth, provider);
  } catch (error) {
    console.error("Error signing in with Google", error);
  }
}

export async function signOut() {
  try {
    return auth.signOut();
  } catch (error) {
    console.error("Error signing out with Google", error);
  }
}

This code uses the following Firebase APIs:

Firebase API

Description

GoogleAuthProvider

Creates a Google authentication provider instance.

signInWithPopup

Starts a dialog-based authentication flow.

auth.signOut

Signs out the user.

In the src/components/Header.jsx file, the code already invokes the signInWithGoogle and signOut functions.

  1. Create a commit with commit message "Adding Google Authentication" and push it to your GitHub repository. 1. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  2. In the web app, refresh the page and click Sign in with Google. The web app doesn't update, so it's unclear whether sign-in succeeded.

Send authentication state to the server

In order to pass authentication state to the server, we'll use a service worker. Replace the fetchWithFirebaseHeaders and getAuthIdToken functions with the following code:

async function fetchWithFirebaseHeaders(request) {
  const app = initializeApp(firebaseConfig);
  const auth = getAuth(app);
  const installations = getInstallations(app);
  const headers = new Headers(request.headers);
  const [authIdToken, installationToken] = await Promise.all([
    getAuthIdToken(auth),
    getToken(installations),
  ]);
  headers.append("Firebase-Instance-ID-Token", installationToken);
  if (authIdToken) headers.append("Authorization", `Bearer ${authIdToken}`);
  const newRequest = new Request(request, { headers });
  return await fetch(newRequest);
}

async function getAuthIdToken(auth) {
  await auth.authStateReady();
  if (!auth.currentUser) return;
  return await getIdToken(auth.currentUser);
}

Read authentication state on the server

We'll use FirebaseServerApp to mirror the client's authentication state on the server.

Open src/lib/firebase/serverApp.js, and replace the getAuthenticatedAppForUser function:

export async function getAuthenticatedAppForUser() {
  const idToken = headers().get("Authorization")?.split("Bearer ")[1];
  console.log('firebaseConfig', JSON.stringify(firebaseConfig));
  const firebaseServerApp = initializeServerApp(
    firebaseConfig,
    idToken
      ? {
          authIdToken: idToken,
        }
      : {}
  );

  const auth = getAuth(firebaseServerApp);
  await auth.authStateReady();

  return { firebaseServerApp, currentUser: auth.currentUser };
}

Subscribe to authentication changes

To subscribe to authentication changes, follow these steps:

  1. Navigate to the src/components/Header.jsx file.
  2. Replace the useUserSession function with the following code:
function useUserSession(initialUser) {
	// The initialUser comes from the server via a server component
	const [user, setUser] = useState(initialUser);
	const router = useRouter();

	// Register the service worker that sends auth state back to server
	// The service worker is built with npm run build-service-worker
	useEffect(() => {
		if ("serviceWorker" in navigator) {
			const serializedFirebaseConfig = encodeURIComponent(JSON.stringify(firebaseConfig));
			const serviceWorkerUrl = `/auth-service-worker.js?firebaseConfig=${serializedFirebaseConfig}`
		
		  navigator.serviceWorker
			.register(serviceWorkerUrl)
			.then((registration) => console.log("scope is: ", registration.scope));
		}
	  }, []);

	useEffect(() => {
		const unsubscribe = onAuthStateChanged((authUser) => {
			setUser(authUser)
		})

		return () => unsubscribe()
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	useEffect(() => {
		onAuthStateChanged((authUser) => {
			if (user === undefined) return

			// refresh when user changed to ease testing
			if (user?.email !== authUser?.email) {
				router.refresh()
			}
		})
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, [user])

	return user;
}

This code uses a React state hook to update the user when the onAuthStateChanged function specifies that there's a change to the authentication state.

Verify changes

The root layout in the src/app/layout.js file renders the header and passes in the user, if available, as a prop.

<Header initialUser={currentUser?.toJSON()} />

This means that the <Header> component renders user data, if available, during server run time. If there are any authentication updates during the page lifecycle after initial page load, the onAuthStateChanged handler handles them.

Now it's time to roll out a new build and verify what you built.

  1. Create a commit with commit message "Show signin state" and push it to your GitHub repository.
  2. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  3. Verify the new authentication behavior:
    1. In your browser, refresh the web app. Your display name appears in the header.
    2. Sign out and sign in again. The page updates in real-time without a page refresh. You can repeat this step with different users.
    3. Optional: Right-click the web app, select View page source, and search for the display name. It appears in the raw HTML source returned from the server.

7. View restaurant information

The web app includes mock data for restaurants and reviews.

Add one or more restaurants

To insert mock restaurant data into your local Cloud Firestore database, follow these steps:

  1. In the web app, select 2cf67d488d8e6332.png > Add sample restaurants.
  2. In the Firebase console on the Firestore Database page, select restaurants. You see the top-level documents in the restaurant collection, each of which represents a restaurant.
  3. Click a few documents to explore the properties of a restaurant document.

Display the list of restaurants

Your Cloud Firestore database now has restaurants that the Next.js web app can display.

To define the data-fetching code, follow these steps:

  1. In the src/app/page.js file, find the <Home /> server component, and review the call to the getRestaurants function, which retrieves a list of restaurants at server run time. You implement the getRestaurants function in the following steps.
  2. In the src/lib/firebase/firestore.js file, replace the applyQueryFilters and getRestaurants functions with the following code:
function applyQueryFilters(q, { category, city, price, sort }) {
	if (category) {
		q = query(q, where("category", "==", category));
	}
	if (city) {
		q = query(q, where("city", "==", city));
	}
	if (price) {
		q = query(q, where("price", "==", price.length));
	}
	if (sort === "Rating" || !sort) {
		q = query(q, orderBy("avgRating", "desc"));
	} else if (sort === "Review") {
		q = query(q, orderBy("numRatings", "desc"));
	}
	return q;
}

export async function getRestaurants(db = db, filters = {}) {
	let q = query(collection(db, "restaurants"));

	q = applyQueryFilters(q, filters);
	const results = await getDocs(q);
	return results.docs.map(doc => {
		return {
			id: doc.id,
			...doc.data(),
			// Only plain objects can be passed to Client Components from Server Components
			timestamp: doc.data().timestamp.toDate(),
		};
	});
}
  1. Create a commit with commit message "Read the list of restaurants from Firestore" and push it to your GitHub repository.
  2. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  3. In the web app, refresh the page. Restaurant images appear as tiles on the page.

Verify that the restaurant listings load at server run time

Using the Next.js framework, it might not be obvious when data is loaded at server run time or client-side run time.

To verify that restaurant listings load at server run time, follow these steps:

  1. In the web app, open DevTools and disable JavaScript.

Disable JavaScipt in DevTools

  1. Refresh the web app. The restaurant listings still load. Restaurant information is returned in the server response. When JavaScript is enabled, the restaurant information is hydrated through the client-side JavaScript code.
  2. In DevTools, re-enable JavaScript.

Listen for restaurant updates with Cloud Firestore snapshot listeners

In the previous section, you saw how the initial set of restaurants loaded from the src/app/page.js file. The src/app/page.js file is a server component and is rendered on the server, including the Firebase data-fetching code.

The src/components/RestaurantListings.jsx file is a client component and can be configured to hydrate server-rendered markup.

To configure the src/components/RestaurantListings.jsx file to hydrate server-rendered markup, follow these steps:

  1. In the src/components/RestaurantListings.jsx file, observe the following code, which is already written for you:
useEffect(() => {
        const unsubscribe = getRestaurantsSnapshot(data => {
                setRestaurants(data);
        }, filters);

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

This code invokes the getRestaurantsSnapshot() function, which is similar to the getRestaurants() function that you implemented in a previous step. However this snapshot function provides a callback mechanism so that the callback is invoked every time a change is made to the restaurant's collection.

  1. In the src/lib/firebase/firestore.js file, replace the getRestaurantsSnapshot() function with the following code:
export function getRestaurantsSnapshot(cb, filters = {}) {
	if (typeof cb !== "function") {
		console.log("Error: The callback parameter is not a function");
		return;
	}

	let q = query(collection(db, "restaurants"));
	q = applyQueryFilters(q, filters);

	const unsubscribe = onSnapshot(q, querySnapshot => {
		const results = querySnapshot.docs.map(doc => {
			return {
				id: doc.id,
				...doc.data(),
				// Only plain objects can be passed to Client Components from Server Components
				timestamp: doc.data().timestamp.toDate(),
			};
		});

		cb(results);
	});

	return unsubscribe;
}

Changes made through the Firestore Database page now reflect in the web app in real time.

  1. Create a commit with commit message "Listen for realtime restaurant updates" and push it to your GitHub repository.
  2. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  3. In the web app, select 27ca5d1e8ed8adfe.png > Add sample restaurants. If your snapshot function is implemented correctly, the restaurants appear in real-time without a page refresh.

8. Save user-submitted reviews from the web app

  1. In the src/lib/firebase/firestore.js file, replace the updateWithRating() function with the following code:
const updateWithRating = async (
	transaction,
	docRef,
	newRatingDocument,
	review
) => {
	const restaurant = await transaction.get(docRef);
	const data = restaurant.data();
	const newNumRatings = data?.numRatings ? data.numRatings + 1 : 1;
	const newSumRating = (data?.sumRating || 0) + Number(review.rating);
	const newAverage = newSumRating / newNumRatings;

	transaction.update(docRef, {
		numRatings: newNumRatings,
		sumRating: newSumRating,
		avgRating: newAverage,
	});

	transaction.set(newRatingDocument, {
		...review,
		timestamp: Timestamp.fromDate(new Date()),
	});
};

This code inserts a new Firestore document representing the new review. The code also updates the existing Firestore document that represents the restaurant with updated figures for the number of ratings and the average calculated rating.

  1. Replace the addReviewToRestaurant() function with the following code:
export async function addReviewToRestaurant(db, restaurantId, review) {
	if (!restaurantId) {
		throw new Error("No restaurant ID has been provided.");
	}

	if (!review) {
		throw new Error("A valid review has not been provided.");
	}

	try {
		const docRef = doc(collection(db, "restaurants"), restaurantId);
		const newRatingDocument = doc(
			collection(db, `restaurants/${restaurantId}/ratings`)
		);

		// corrected line
		await runTransaction(db, transaction =>
			updateWithRating(transaction, docRef, newRatingDocument, review)
		);
	} catch (error) {
		console.error(
			"There was an error adding the rating to the restaurant",
			error
		);
		throw error;
	}
}

Implement a Next.js Server Action

A Next.js Server Action provides a convenient API to access form data, such as data.get("text") to get the text value from the form submission payload.

To use a Next.js Server Action to process the review form submission, follow these steps:

  1. In the src/components/ReviewDialog.jsx file, find the action attribute in the <form> element.
<form action={handleReviewFormSubmission}>

The action attribute value refers to a function that you implement in the next step.

  1. In the src/app/actions.js file, replace the handleReviewFormSubmission() function with the following code:
// This is a next.js server action, which is an alpha feature, so
// use with caution.
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
export async function handleReviewFormSubmission(data) {
        const { app } = await getAuthenticatedAppForUser();
        const db = getFirestore(app);

        await addReviewToRestaurant(db, data.get("restaurantId"), {
                text: data.get("text"),
                rating: data.get("rating"),

                // This came from a hidden form field.
                userId: data.get("userId"),
        });
}

Add reviews for a restaurant

You implemented support for review submissions, so now you can verify that your reviews are inserted into Cloud Firestore correctly.

To add a review and verify that it's inserted into Cloud Firestore, follow these steps:

  1. Create a commit with commit message "Allow users to submit restaurant reviews" and push it to your GitHub repository.
  2. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  3. Refresh the web app, and select a restaurant from the home page.
  4. On the restaurant's page, click 3e19beef78bb0d0e.png.
  5. Select a star rating.
  6. Write a review.
  7. Click Submit. Your review appears at the top of the list of reviews.
  8. In Cloud Firestore, search the Add document pane for the document of the restaurant that you reviewed and select it.
  9. In the Start collection pane, select ratings.
  10. In the Add document pane, find the document for your review to verify that it was inserted as expected.

Documents in the Firestore Emulator

9. Save user-uploaded files from the web app

In this section, you add functionality so that you can replace the image associated with a restaurant when you're logged in. You upload the image to Firebase Storage, and update the image URL in the Cloud Firestore document that represents the restaurant.

To save user-uploaded files from the web app, follow these steps:

  1. In the src/components/Restaurant.jsx file, observe the code that runs when the user uploads a file:
async function handleRestaurantImage(target) {
        const image = target.files ? target.files[0] : null;
        if (!image) {
                return;
        }

        const imageURL = await updateRestaurantImage(id, image);
        setRestaurant({ ...restaurant, photo: imageURL });
}

No changes are needed, but you implement the behavior of the updateRestaurantImage() function in the following steps.

  1. In the src/lib/firebase/storage.js file, replace the updateRestaurantImage() and uploadImage() functions with the following code:
export async function updateRestaurantImage(restaurantId, image) {
	try {
		if (!restaurantId)
			throw new Error("No restaurant ID has been provided.");

		if (!image || !image.name)
			throw new Error("A valid image has not been provided.");

		const publicImageUrl = await uploadImage(restaurantId, image);
		await updateRestaurantImageReference(restaurantId, publicImageUrl);

		return publicImageUrl;
	} catch (error) {
		console.error("Error processing request:", error);
	}
}

async function uploadImage(restaurantId, image) {
	const filePath = `images/${restaurantId}/${image.name}`;
	const newImageRef = ref(storage, filePath);
	await uploadBytesResumable(newImageRef, image);

	return await getDownloadURL(newImageRef);
}

The updateRestaurantImageReference() function is already implemented for you. This function updates an existing restaurant document in Cloud Firestore with an updated image URL.

Verify the image-upload functionality

To verify that the image uploads as expected, follow these steps:

  1. Create a commit with commit message "Allow users to change each restaurants' photo" and push it to your GitHub repository.
  2. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  3. In the web app, verify that you're logged in and select a restaurant.
  4. Click 7067eb41fea41ff0.png and upload an image from your filesystem. Your image leaves your local environment and is uploaded to Cloud Storage. The image appears immediately after you upload it.
  5. Navigate to Cloud Storage for Firebase.
  6. Navigate to the folder that represents the restaurant. The image that you uploaded exists in the folder.

6cf3f9e2303c931c.png

10. Summarize restaurant reviews with generative ai

In this section, you'll add a review summary feature so that a user can quickly understand what everyone thinks of a restaurant without having to read every review.

Store a Gemini API key in Cloud Secret Manager

  1. To use the Gemini API, you'll need an API key. Create a key in Google AI Studio.
  2. App Hosting integrates with Cloud Secret Manager to allow you to store sensitive values like API keys securely:
    1. In a terminal, run the command to create a new secret:
    firebase apphosting:secrets:set gemini-api-key
    
    1. When prompted for the secret value, copy and paste your Gemini API key from Google AI Studio.
    2. When asked if the new secret should be added to apphosting.yaml, enter Y to accept.

Your Gemini API key is now stored securely in Cloud Secret manager, and is accessible to your App Hosting backend.

Implement the review summary component

  1. In src/components/Reviews/ReviewSummary.jsx, replace the GeminiSummary function with the following code:
    export async function GeminiSummary({ restaurantId }) {
        const { firebaseServerApp } = await getAuthenticatedAppForUser();
        const reviews = await getReviewsByRestaurantId(
            getFirestore(firebaseServerApp),
            restaurantId
        );
    
        const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
        const model = genAI.getGenerativeModel({ model: "gemini-pro"});
    
        const reviewSeparator = "@";
        const prompt = `
            Based on the following restaurant reviews, 
            where each review is separated by a '${reviewSeparator}' character, 
            create a one-sentence summary of what people think of the restaurant. 
    
            Here are the reviews: ${reviews.map(review => review.text).join(reviewSeparator)}
        `;
    
        try {
            const result = await model.generateContent(prompt);
            const response = await result.response;
            const text = response.text();
    
            return (
                <div className="restaurant__review_summary">
                    <p>{text}</p>
                    <p>✨ Summarized with Gemini</p>
                </div>
            );
        } catch (e) {
            console.error(e);
            return <p>Error contacting Gemini</p>;
        }
    }
    
  2. Create a commit with commit message "Use AI to summarize reviews" and push it to your GitHub repository.
  3. Open the App Hosting page in the Firebase console and wait for your new rollout to complete.
  4. Open a page for a restaurant. At the top, you should see a one-sentence summary of all the reviews on the page.
  5. Add a new review and refresh the page. You should see the summary change.

11. Conclusion

Congratulations! You learned how to use Firebase to add features and functionality to a Next.js app. Specifically, you used the following:

Learn more