Integra Firebase a una app de Next.js

1. Antes de comenzar

En este codelab, aprenderás a integrar Firebase a una app web de Next.js llamada Friendly Eats, que es un sitio web para opiniones sobre restaurantes.

App web de Friendly Eats

La app web completa ofrece funciones útiles que demuestran cómo Firebase puede ayudarte a compilar apps de Next.js. Entre estas características, se incluyen las siguientes:

  • Compilación e implementación automáticas: En este codelab, se usa Firebase App Hosting para compilar e implementar automáticamente tu código de Next.js cada vez que envíes a una rama configurada.
  • Acceder y salir: La app web completa te permite acceder con Google y salir. El acceso y la persistencia de los usuarios se administran completamente a través de Firebase Authentication.
  • Imágenes: La app web completa permite que los usuarios que accedieron suban imágenes de restaurantes. Los recursos de imagen se almacenan en Cloud Storage para Firebase. El SDK de Firebase JavaScript proporciona una URL pública para las imágenes subidas. Luego, esta URL pública se almacena en el documento relevante del restaurante en Cloud Firestore.
  • Opiniones: La aplicación web completa les permite a los usuarios que acceden publicar opiniones sobre restaurantes que constan de una calificación por estrellas y un mensaje de texto. La información de las opiniones se almacena en Cloud Firestore.
  • Filtros: La aplicación web completa les permite a los usuarios que acceden filtrar la lista de restaurantes según la categoría, la ubicación y el precio. También puedes personalizar el método de ordenamiento que se usa. Se accede a los datos desde Cloud Firestore, y las consultas de Firestore se aplican según los filtros que se usen.

Requisitos previos

  • Una cuenta de GitHub
  • Conocimientos de Next.js y JavaScript

Qué aprenderás

  • Cómo usar Firebase con el router de apps de Next.js y la renderización del servidor
  • Cómo conservar imágenes en Cloud Storage para Firebase.
  • Cómo leer y escribir datos en una base de datos de Cloud Firestore
  • Cómo usar el acceso con Google con el SDK de Firebase JavaScript.

Requisitos

  • Git
  • Una versión estable reciente de Node.js
  • El navegador que elijas, como Google Chrome
  • Un entorno de desarrollo con un editor de código y una terminal
  • Una Cuenta de Google para crear y administrar tu proyecto de Firebase
  • La capacidad de actualizar tu proyecto de Firebase al plan de precios Blaze

2. Configura tu entorno de desarrollo y repositorio de GitHub

En este codelab, se proporciona la base de código de partida de la app y se basa en Firebase CLI.

Crea un repositorio de GitHub

Puedes encontrar la fuente del codelab en https://github.com/firebase/friendlyeats-web. El repositorio contiene proyectos de muestra para varias plataformas. Sin embargo, en este codelab, solo se usa el directorio nextjs-start. Toma nota de los siguientes directorios:

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

Copia la carpeta nextjs-start en tu propio repositorio:

  1. Con una terminal, crea una carpeta nueva en tu computadora y cambia al directorio nuevo:
    mkdir codelab-friendlyeats-web
    
    cd codelab-friendlyeats-web
    
  2. Usa el paquete npm giget para recuperar solo la carpeta nextjs-start:
    npx giget@latest gh:firebase/friendlyeats-web/nextjs-start#master . --install
    
  3. Haz un seguimiento de los cambios de forma local con git:
    git init
    
    git commit -a -m "codelab starting point"
    
    git branch -M main
    
  4. Crea un repositorio de GitHub nuevo: https://github.com/new. Ponle el nombre que desees.
    1. GitHub te dará una nueva URL de repositorio que se verá como https://github.com//.git o git@github.com:/.git. Copia esta URL.
  5. Envía los cambios locales a tu nuevo repositorio de GitHub. Ejecuta el siguiente comando y sustituye la URL del repositorio por el marcador de posición .
    git remote add origin <your-repository-url>
    
    git push -u origin main
    
  6. Ahora deberías ver el código de partida en tu repositorio de GitHub.

Instala o actualiza Firebase CLI

Ejecuta el siguiente comando para verificar que tienes Firebase CLI instalado y que es v13.9.0 o una versión posterior:

firebase --version

Si ves una versión anterior o no tienes Firebase CLI instalado, ejecuta el comando de instalación:

npm install -g firebase-tools@latest

Si no puedes instalar Firebase CLI debido a errores de permisos, consulta la documentación de npm o usa otra opción de instalación.

Accede a Firebase.

  1. Ejecuta el siguiente comando para acceder a Firebase CLI:
    firebase login
    
  2. Según si deseas que Firebase recopile datos, ingresa Y o N.
  3. En tu navegador, selecciona tu Cuenta de Google y, luego, haz clic en Permitir.

3. Configura el proyecto de Firebase

En esta sección, configurarás un proyecto de Firebase y asociarás una app web de Firebase con él. También configurarás los servicios de Firebase que usa la app web de ejemplo.

Crea un proyecto de Firebase

  1. En Firebase console, haz clic en Agregar proyecto.
  2. En el cuadro de texto Ingresa el nombre de proyecto, ingresa FriendlyEats Codelab (o un nombre de proyecto que prefieras) y haz clic en Continuar.
  3. En la ventana modal Confirmar plan de facturación de Firebase, confirma que el plan sea Blaze y, luego, haz clic en Confirmar plan.
  4. Para este codelab, no necesitas Google Analytics, así que desactiva la opción Habilitar Google Analytics para este proyecto.
  5. Haz clic en Crear proyecto.
  6. Espera a que se aprovisione tu proyecto y, luego, haz clic en Continuar.
  7. En tu proyecto de Firebase, ve a Configuración del proyecto. Anota el ID de tu proyecto porque lo necesitarás más adelante. Este identificador único es la forma en que se identifica tu proyecto (por ejemplo, en Firebase CLI).

Actualiza tu plan de precios de Firebase

Para usar App Hosting, tu proyecto de Firebase debe tener el plan de precios Blaze, lo que significa que está asociado a una cuenta de Facturación de Cloud.

  • Una cuenta de Facturación de Cloud requiere una forma de pago, como una tarjeta de crédito.
  • Si es la primera vez que usas Firebase y Google Cloud, verifica si cumples con los requisitos para obtener un crédito de USD 300 y una cuenta de Facturación de Cloud de prueba gratuita.

Para actualizar tu proyecto al plan Blaze, sigue estos pasos:

  1. En Firebase console, selecciona la opción para actualizar tu plan.
  2. En el diálogo, selecciona el plan Blaze y, luego, sigue las instrucciones en pantalla para asociar tu proyecto con una cuenta de Facturación de Cloud.
    Si necesitas crear una cuenta de Facturación de Cloud, es posible que debas volver a la actualización. en Firebase console para completar la actualización.

Agrega una app web a tu proyecto de Firebase

  1. Navega a la Descripción general del proyecto en tu proyecto de Firebase y, luego, haz clic en e41f2efdd9539c31.png Web.

    Si ya tienes apps registradas en tu proyecto, haz clic en Agregar app para ver el ícono de Web.
  2. En el cuadro de texto App nickname, ingresa un sobrenombre fácil de recordar, como My Next.js app.
  3. Deja sin marcar la casilla de verificación Configurar Firebase Hosting para esta app también.
  4. Haz clic en Registrar app > Siguiente > Siguiente > Ir a la consola.

Configura los servicios de Firebase en Firebase console

Configura la autenticación

  1. En Firebase console, navega a Autenticación.
  2. Haz clic en Comenzar.
  3. En la columna Proveedores adicionales, haz clic en Google > Habilitar.
  4. En el cuadro de texto Nombre público del proyecto, ingresa un nombre fácil de recordar, como My Next.js app.
  5. En el menú desplegable Correo electrónico de asistencia para el proyecto, selecciona tu dirección de correo electrónico.
  6. Haz clic en Guardar.

Configura Cloud Firestore

  1. En Firebase console, navega a Firestore.
  2. Haz clic en Crear base de datos > Siguiente > Iniciar en modo de prueba > A continuación.
    Más adelante en este codelab, agregarás reglas de seguridad para proteger tus datos. No distribuyas ni expongas una app públicamente sin agregar reglas de seguridad para tu base de datos.
  3. Usa la ubicación predeterminada o selecciona una que elijas.
    En el caso de una app real, te recomendamos que elijas una ubicación cercana a los usuarios. Ten en cuenta que esta ubicación no se puede cambiar más adelante y que también será automáticamente la ubicación de tu bucket predeterminado de Cloud Storage (próximo paso).
  4. Haz clic en Listo.

Configura Cloud Storage para Firebase

  1. En Firebase console, navega a Storage.
  2. Haz clic en Get started > Start in test mode > Next.
    Más adelante en este codelab, agregarás reglas de seguridad para proteger tus datos. No distribuyas ni expongas una app de forma pública sin agregar reglas de seguridad para tu bucket de Storage.
  3. La ubicación de tu bucket ya debería estar seleccionada (debido a que configuraste Firestore en el paso anterior).
  4. Haz clic en Listo.

4. Revisa la base de código de partida

En esta sección, revisarás algunas áreas de la base de código inicial de la app a las que agregarás funcionalidad en este codelab.

Estructura de carpetas y archivos

En la siguiente tabla, se incluye una descripción general de la estructura de carpetas y archivos de la app:

Carpetas y archivos

Descripción

src/components

Componentes de React para filtros, encabezados, detalles de restaurantes y opiniones

src/lib

Funciones de utilidad que no están necesariamente vinculadas a React o Next.js

src/lib/firebase

Código específico y configuración de Firebase

public

Elementos estáticos en la app web, como íconos

src/app

Enrutamiento con el router de apps de Next.js

src/app/restaurant

Un controlador de ruta de la API

package.json y package-lock.json

Dependencias del proyecto con npm

next.config.js

Configuración específica de Next.js (las acciones del servidor están habilitadas)

jsconfig.json

Configuración del servicio de lenguaje JavaScript

Componentes del servidor y el cliente

La app es una app web de Next.js que usa el router de app. El procesamiento del servidor se usa en toda la app. Por ejemplo, el archivo src/app/page.js es un componente del servidor responsable de la página principal. El archivo src/components/RestaurantListings.jsx es un componente de cliente indicado por la directiva "use client" al comienzo del archivo.

Declaraciones de importación

Es posible que observes sentencias de importación como las siguientes:

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

La app usa el símbolo @ para evitar rutas de acceso de importación relativas complicadas y es posible gracias a los alias de la ruta de acceso.

APIs específicas de Firebase

Todo el código de la API de Firebase se une en el directorio src/lib/firebase. Luego, los componentes individuales de React importan las funciones unidas desde el directorio src/lib/firebase, en lugar de importar funciones de Firebase directamente.

Datos de prueba

Los datos simulados de restaurantes y opiniones se incluyen en el archivo src/lib/randomData.js. Los datos de ese archivo se ensamblan en el código del archivo src/lib/fakeRestaurants.js.

5. Crea un backend de App Hosting

En esta sección, configurarás un backend de App Hosting para supervisar una rama en tu repositorio de git.

Al final de esta sección, tendrás un backend de App Hosting conectado a tu repositorio en GitHub que volverá a compilar y lanzará una versión nueva de tu app automáticamente cada vez que envíes una confirmación nueva a tu rama main.

Implementa reglas de seguridad

El código ya tiene conjuntos de reglas de seguridad para Firestore y Cloud Storage para Firebase. Después de implementar las reglas de seguridad, los datos de tu base de datos y tu bucket estarán mejor protegidos contra el uso inadecuado.

  1. En tu terminal, configura la CLI para usar el proyecto de Firebase que creaste anteriormente:
    firebase use --add
    
    Cuando se te solicite un alias, ingresa friendlyeats-codelab.
  2. Para implementar estas reglas de seguridad, ejecuta este comando en la terminal:
    firebase deploy --only firestore:rules,storage
    
  3. Si se te pregunta: "Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?", presiona Enter para seleccionar .

Agrega tu configuración de Firebase al código de tu app web

  1. En Firebase console, navega a Configuración del proyecto.
  2. En el panel SDK setup and configuration, haz clic en "Add app" y, luego, en el ícono de corchetes de código para registrar una nueva app web.
  3. Al final del flujo de creación de la app web, copia la variable firebaseConfig y sus propiedades y valores.
  4. Abre el archivo apphosting.yaml en el editor de código y completa los valores de la variable de entorno con los valores de configuración de Firebase console.
  5. En el archivo, reemplaza las propiedades existentes por las que copiaste.
  6. Guarda el archivo.

Crear un backend

  1. Ve a la página Hosting de apps en Firebase console:

El estado cero de la consola de App Hosting, con un “Primeros pasos” botón

  1. Haz clic en "Comenzar" para iniciar el flujo de creación del backend. Configura tu backend de la siguiente manera:
  2. Sigue las indicaciones del primer paso para conectar el repositorio de GitHub que creaste antes.
  3. Establece la configuración de implementación:
    1. Mantén el directorio raíz como /.
    2. Establece la rama publicada como main
    3. Habilitar los lanzamientos automáticos
  4. Asigna el nombre friendlyeats-codelab al backend.
  5. En "Crea o asocia una app web de Firebase", elige la app web que configuraste antes en el menú desplegable "Seleccionar una app web de Firebase existente".
  6. Haz clic en "Finalizar e implementar". Después de un momento, se te dirigirá a una página nueva en la que podrás ver el estado de tu nuevo backend de App Hosting.
  7. Cuando se complete el lanzamiento, haz clic en tu dominio gratuito en la sección “dominios”. Esto puede tardar unos minutos en comenzar a funcionar debido a la propagación de DNS.

Implementaste la app web inicial. Cada vez que envíes una confirmación nueva a la rama main de tu repositorio de GitHub, verás que comienza una nueva compilación y lanzamiento en Firebase console, y tu sitio se actualizará automáticamente una vez que se complete el lanzamiento.

6. Agrega autenticación a la app web

En esta sección, agregarás autenticación a la app web para poder acceder a ella.

Implementa las funciones de acceso y cierre de sesión

  1. En el archivo src/lib/firebase/auth.js, reemplaza las funciones onAuthStateChanged, signInWithGoogle y signOut por el siguiente código:
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);
  }
}

En este código, se usan las siguientes APIs de Firebase:

API de Firebase

Descripción

GoogleAuthProvider

Crea una instancia del proveedor de autenticación de Google.

signInWithPopup

Inicia un flujo de autenticación basado en diálogos.

auth.signOut

Sale del usuario.

En el archivo src/components/Header.jsx, el código ya invoca las funciones signInWithGoogle y signOut.

  1. Crea una confirmación con el mensaje "Agrega la autenticación de Google" y envíala a tu repositorio de GitHub. 1. Abre la página App Hosting en Firebase console y espera a que se complete el lanzamiento nuevo.
  2. En la app web, actualiza la página y haz clic en Acceder con Google. La app web no se actualiza, por lo que no está claro si el acceso se realizó correctamente.

Envía el estado de autenticación al servidor

Para pasar el estado de autenticación al servidor, usaremos un trabajador de servicio. Reemplaza las funciones fetchWithFirebaseHeaders y getAuthIdToken por el siguiente código:

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);
}

Lee el estado de autenticación en el servidor

Usaremos FirebaseServerApp para duplicar el estado de autenticación del cliente en el servidor.

Abre src/lib/firebase/serverApp.js y reemplaza la función getAuthenticatedAppForUser:

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 };
}

Suscríbete a los cambios de autenticación

Para suscribirte a los cambios de autenticación, sigue estos pasos:

  1. Navega al archivo src/components/Header.jsx.
  2. Reemplaza la función useUserSession por el siguiente código:
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;
}

Este código usa un hook de estado de React para actualizar al usuario cuando la función onAuthStateChanged especifica que hay un cambio en el estado de autenticación.

Verifica los cambios

El diseño raíz en el archivo src/app/layout.js renderiza el encabezado y pasa al usuario, si está disponible, como un elemento.

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

Esto significa que el componente <Header> renderiza los datos del usuario, si están disponibles, durante el tiempo de ejecución del servidor. Si hay actualizaciones de autenticación durante el ciclo de vida de la página después de la carga inicial de la página, el controlador onAuthStateChanged las controlará.

Ahora es momento de lanzar una compilación nueva y verificar lo que compilaste.

  1. Crea una confirmación con el mensaje de confirmación “Mostrar el estado de acceso” y enviarla a tu repositorio de GitHub.
  2. Abre la página de Hosting de apps en Firebase console y espera a que se complete el lanzamiento nuevo.
  3. Verifica el nuevo comportamiento de autenticación:
    1. En tu navegador, actualiza la app web. Tu nombre visible aparecerá en el encabezado.
    2. Sal y vuelve a acceder. La página se actualiza en tiempo real sin tener que actualizar la página. Puedes repetir este paso con diferentes usuarios.
    3. Opcional: Haz clic con el botón derecho en la aplicación web, selecciona Ver el código fuente de la página y busca el nombre visible. Aparecerá en la fuente HTML sin procesar que muestra el servidor.

7. Visualiza información del restaurante

La app web incluye datos de muestra de restaurantes y opiniones.

Cómo agregar uno o más restaurantes

Para insertar datos de restaurantes ficticios en tu base de datos local de Cloud Firestore, sigue estos pasos:

  1. En la app web, selecciona 2cf67d488d8e6332.png > Agregar restaurantes de ejemplo.
  2. En la página Base de datos de Firestore de Firebase console, selecciona restaurantes. Verás los documentos de nivel superior en la colección de restaurantes, cada uno de los cuales representa un restaurante.
  3. Haz clic en algunos documentos para explorar las propiedades de un documento de restaurante.

Muestra la lista de restaurantes

Tu base de datos de Cloud Firestore ahora tiene restaurantes que la app web Next.js puede mostrar.

Para definir el código de recuperación de datos, sigue estos pasos:

  1. En el archivo src/app/page.js, busca el componente del servidor <Home /> y revisa la llamada a la función getRestaurants, que recupera una lista de restaurantes en el tiempo de ejecución del servidor. Implementarás la función getRestaurants en los siguientes pasos.
  2. En el archivo src/lib/firebase/firestore.js, reemplaza las funciones applyQueryFilters y getRestaurants por el siguiente código:
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. Crea una confirmación con el mensaje de confirmación “Lee la lista de restaurantes de Firestore” y enviarla a tu repositorio de GitHub.
  2. Abre la página de Hosting de apps en Firebase console y espera a que se complete el lanzamiento nuevo.
  3. En la app web, actualiza la página. Las imágenes del restaurante aparecen como mosaicos en la página.

Verifica que las fichas de restaurantes se carguen durante el tiempo de ejecución del servidor

Si usas el framework de Next.js, es posible que no sea obvio cuándo los datos se cargan durante el tiempo de ejecución del servidor o del lado del cliente.

Sigue estos pasos para verificar que las fichas de restaurantes se cargan durante el tiempo de ejecución del servidor:

  1. En la app web, abre Herramientas para desarrolladores y inhabilita JavaScript.

Inhabilita JavaScipt en Herramientas para desarrolladores

  1. Actualiza la app web. Las fichas de restaurantes siguen cargando. La información del restaurante se muestra en la respuesta del servidor. Cuando se habilita JavaScript, la información del restaurante se hidrata a través del código JavaScript del cliente.
  2. En Herramientas para desarrolladores, vuelve a habilitar JavaScript.

Detecta actualizaciones de restaurantes con los objetos de escucha de instantáneas de Cloud Firestore

En la sección anterior, viste cómo se cargaba el conjunto inicial de restaurantes desde el archivo src/app/page.js. El archivo src/app/page.js es un componente del servidor y se renderiza en él, incluido el código de recuperación de datos de Firebase.

El archivo src/components/RestaurantListings.jsx es un componente cliente y se puede configurar para hidratar el marcado renderizado por el servidor.

Para configurar el archivo src/components/RestaurantListings.jsx para que active el marcado renderizado por el servidor, sigue estos pasos:

  1. En el archivo src/components/RestaurantListings.jsx, observa el siguiente código, que ya está escrito:
useEffect(() => {
        const unsubscribe = getRestaurantsSnapshot(data => {
                setRestaurants(data);
        }, filters);

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

Este código invoca la función getRestaurantsSnapshot(), que es similar a la función getRestaurants() que implementaste en un paso anterior. Sin embargo, esta función de instantánea proporciona un mecanismo de devolución de llamada para que se invoque cada vez que se realice un cambio en la colección del restaurante.

  1. En el archivo src/lib/firebase/firestore.js, reemplaza la función getRestaurantsSnapshot() por el siguiente código:
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;
}

Los cambios realizados a través de la página Base de datos de Firestore ahora se reflejan en la app web en tiempo real.

  1. Crea una confirmación con el mensaje “Escucha actualizaciones de restaurantes en tiempo real” y enviarla a tu repositorio de GitHub.
  2. Abre la página de Hosting de apps en Firebase console y espera a que se complete el lanzamiento nuevo.
  3. En la aplicación web, selecciona 27ca5d1e8ed8adfe.png > Agrega restaurantes de muestra. Si tu función de instantánea se implementa correctamente, los restaurantes aparecerán en tiempo real sin tener que actualizar la página.

8. Cómo guardar las opiniones enviadas por usuarios desde la app web

  1. En el archivo src/lib/firebase/firestore.js, reemplaza la función updateWithRating() por el siguiente código:
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()),
	});
};

Este código inserta un nuevo documento de Firestore que representa la nueva opinión. El código también actualiza el documento de Firestore existente que representa el restaurante con las cifras actualizadas de la cantidad de calificaciones y la calificación promedio calculada.

  1. Reemplaza la función addReviewToRestaurant() por el siguiente código:
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;
	}
}

Implementa una acción del servidor de Next.js

Una acción del servidor de Next.js proporciona una API conveniente para acceder a los datos del formulario, como data.get("text"), para obtener el valor de texto de la carga útil de envío de formulario.

Para usar una acción de servidor de Next.js y procesar el envío del formulario de revisión, sigue estos pasos:

  1. En el archivo src/components/ReviewDialog.jsx, busca el atributo action en el elemento <form>.
<form action={handleReviewFormSubmission}>

El valor del atributo action hace referencia a una función que implementarás en el siguiente paso.

  1. En el archivo src/app/actions.js, reemplaza la función handleReviewFormSubmission() por el siguiente código:
// 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"),
        });
}

Agrega opiniones sobre un restaurante

Implementaste la compatibilidad con los envíos de opiniones, por lo que ahora puedes verificar que tus opiniones se inserten correctamente en Cloud Firestore.

Para agregar una opinión y verificar que se inserte en Cloud Firestore, sigue estos pasos:

  1. Crea una confirmación con el mensaje "Permitir que los usuarios envíen opiniones sobre restaurantes" y envíala a tu repositorio de GitHub.
  2. Abre la página de Hosting de apps en Firebase console y espera a que se complete el lanzamiento nuevo.
  3. Actualiza la app web y selecciona un restaurante en la página principal.
  4. En la página del restaurante, haz clic en 3e19beef78bb0d0e.png.
  5. Selecciona una calificación por estrellas.
  6. Escribe una opinión
  7. Haz clic en Enviar. Tu opinión aparecerá en la parte superior de la lista de opiniones.
  8. En Cloud Firestore, busca en el panel Agregar documento el documento del restaurante que revisaste y selecciónalo.
  9. En el panel Iniciar colección, selecciona calificaciones.
  10. En el panel Agregar documento, busca el documento que quieres revisar para verificar que se haya insertado como se esperaba.

Documentos en el emulador de Firestore

9. Guarda los archivos subidos por los usuarios desde la app web

En esta sección, agregarás funcionalidad para que puedas reemplazar la imagen asociada con un restaurante cuando hayas accedido. Sube la imagen a Firebase Storage y actualiza la URL de la imagen en el documento de Cloud Firestore que representa al restaurante.

Para guardar los archivos subidos por los usuarios desde la app web, sigue estos pasos:

  1. En el archivo src/components/Restaurant.jsx, observa el código que se ejecuta cuando el usuario sube un archivo:
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 es necesario realizar cambios, pero debes implementar el comportamiento de la función updateRestaurantImage() en los siguientes pasos.

  1. En el archivo src/lib/firebase/storage.js, reemplaza las funciones updateRestaurantImage() y uploadImage() por el siguiente código:
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);
}

La función updateRestaurantImageReference() ya está implementada. Esta función actualiza un documento de restaurante existente en Cloud Firestore con una URL de imagen actualizada.

Verifica la funcionalidad de carga de imágenes

Para verificar que la imagen se cargue como se espera, sigue estos pasos:

  1. Crea una confirmación con el mensaje de confirmación “Allow users to change each customers' foto" y enviarla a tu repositorio de GitHub.
  2. Abre la página App Hosting en Firebase console y espera a que se complete el lanzamiento nuevo.
  3. En la app web, verifica que hayas accedido y selecciona un restaurante.
  4. Haz clic en 7067eb41fea41ff0.png y sube una imagen de tu sistema de archivos. La imagen sale de tu entorno local y se sube a Cloud Storage. La imagen aparecerá inmediatamente después de que la subas.
  5. Navega a Cloud Storage para Firebase.
  6. Navega a la carpeta que representa al restaurante. La imagen que subiste existe en la carpeta.

6cf3f9e2303c931c.png

10. Resume opiniones sobre restaurantes con la IA generativa

En esta sección, agregarás una función de resumen de opiniones para que los usuarios puedan comprender rápidamente lo que piensan todos acerca de un restaurante sin tener que leer cada opinión.

Almacena una clave de API de Gemini en Cloud Secret Manager

  1. Para usar la API de Gemini, necesitarás una clave de API. Crea una clave en Google AI Studio.
  2. App Hosting se integra en Cloud Secret Manager para permitirte almacenar valores sensibles, como claves de API, de forma segura:
    1. En una terminal, ejecuta el comando para crear un secreto nuevo:
    firebase apphosting:secrets:set gemini-api-key
    
    1. Cuando se te solicite el valor secreto, copia y pega tu clave de la API de Gemini desde Google AI Studio.
    2. Cuando se te pregunte si se debe agregar el secreto nuevo a apphosting.yaml, ingresa Y para aceptarlo.

Tu clave de API de Gemini ahora se almacena de forma segura en Secret Manager de Cloud, y tu backend de App Hosting puede acceder a ella.

Implementa el componente de resumen de opiniones

  1. En src/components/Reviews/ReviewSummary.jsx, reemplaza la función GeminiSummary por el siguiente código:
    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. Crea una confirmación con el mensaje "Usar IA para resumir opiniones" y envíala a tu repositorio de GitHub.
  3. Abre la página de Hosting de apps en Firebase console y espera a que se complete el lanzamiento nuevo.
  4. Abre la página de un restaurante. En la parte superior, deberías ver un resumen de una oración con todas las opiniones de la página.
  5. Agrega una opinión nueva y actualiza la página. Deberías ver el cambio en el resumen.

11. Conclusión

¡Felicitaciones! Aprendiste a usar Firebase para agregar funciones y funcionalidades a una app de Next.js. Específicamente, usaste lo siguiente:

Más información