From ddc901f79f19dc0968e3be35231a4d5e026f54bd Mon Sep 17 00:00:00 2001
From: Jeff <3759507+jhuleatt@users.noreply.github.com>
Date: Tue, 14 May 2024 14:55:31 -0400
Subject: [PATCH] update to Next 14 and App Hosting (#287)
---
nextjs-end/.gitignore | 3 +-
nextjs-end/apphosting.yaml | 17 +++
nextjs-end/auth-service-worker.js | 45 ++++++++
nextjs-end/functions/.gitignore | 1 -
nextjs-end/functions/index.js | 19 ----
nextjs-end/functions/package.json | 23 ----
nextjs-end/package.json | 24 ++--
nextjs-end/src/app/actions.js | 9 +-
nextjs-end/src/app/layout.js | 10 +-
nextjs-end/src/app/page.js | 5 +-
nextjs-end/src/app/restaurant/[id]/page.jsx | 58 +++++-----
nextjs-end/src/app/styles.css | 47 +++++++-
nextjs-end/src/components/Header.jsx | 25 ++++-
nextjs-end/src/components/Restaurant.jsx | 46 +++-----
.../src/components/RestaurantDetails.jsx | 2 +
nextjs-end/src/components/ReviewDialog.jsx | 27 ++++-
nextjs-end/src/components/Reviews/Review.jsx | 23 ++++
.../src/components/Reviews/ReviewSummary.jsx | 57 ++++++++++
.../src/components/Reviews/ReviewsList.jsx | 37 ++++++
.../components/Reviews/ReviewsListClient.jsx | 45 ++++++++
nextjs-end/src/components/ReviewsList.jsx | 39 -------
nextjs-end/src/lib/firebase/auth.js | 2 +-
nextjs-end/src/lib/firebase/clientApp.js | 12 ++
nextjs-end/src/lib/firebase/config.js | 23 +++-
nextjs-end/src/lib/firebase/firebase.js | 106 ------------------
nextjs-end/src/lib/firebase/firestore.js | 9 +-
nextjs-end/src/lib/firebase/serverApp.js | 13 +++
nextjs-end/src/lib/firebase/storage.js | 2 +-
nextjs-end/src/lib/getUser.js | 41 +++----
nextjs-start/.gitignore | 3 +-
nextjs-start/apphosting.yaml | 14 +++
nextjs-start/auth-service-worker.js | 34 ++++++
nextjs-start/firebase.json | 69 +++++-------
nextjs-start/package.json | 24 ++--
nextjs-start/src/app/actions.js | 6 +-
nextjs-start/src/app/layout.js | 10 +-
nextjs-start/src/app/page.js | 5 +-
nextjs-start/src/app/restaurant/[id]/page.jsx | 57 +++++-----
nextjs-start/src/app/styles.css | 47 +++++++-
nextjs-start/src/components/Header.jsx | 8 +-
nextjs-start/src/components/Restaurant.jsx | 46 +++-----
.../src/components/RestaurantDetails.jsx | 2 +
nextjs-start/src/components/ReviewDialog.jsx | 27 ++++-
.../src/components/Reviews/Review.jsx | 23 ++++
.../src/components/Reviews/ReviewSummary.jsx | 20 ++++
.../src/components/Reviews/ReviewsList.jsx | 37 ++++++
.../components/Reviews/ReviewsListClient.jsx | 45 ++++++++
nextjs-start/src/components/ReviewsList.jsx | 39 -------
nextjs-start/src/lib/firebase/auth.js | 2 +-
nextjs-start/src/lib/firebase/clientApp.js | 12 ++
nextjs-start/src/lib/firebase/config.js | 23 +++-
nextjs-start/src/lib/firebase/firebase.js | 104 -----------------
nextjs-start/src/lib/firebase/firestore.js | 9 +-
nextjs-start/src/lib/firebase/serverApp.js | 27 +++++
nextjs-start/src/lib/firebase/storage.js | 2 +-
nextjs-start/src/lib/getUser.js | 41 +++----
56 files changed, 882 insertions(+), 624 deletions(-)
create mode 100644 nextjs-end/apphosting.yaml
create mode 100644 nextjs-end/auth-service-worker.js
delete mode 100644 nextjs-end/functions/.gitignore
delete mode 100644 nextjs-end/functions/index.js
delete mode 100644 nextjs-end/functions/package.json
create mode 100644 nextjs-end/src/components/Reviews/Review.jsx
create mode 100644 nextjs-end/src/components/Reviews/ReviewSummary.jsx
create mode 100644 nextjs-end/src/components/Reviews/ReviewsList.jsx
create mode 100644 nextjs-end/src/components/Reviews/ReviewsListClient.jsx
delete mode 100644 nextjs-end/src/components/ReviewsList.jsx
create mode 100644 nextjs-end/src/lib/firebase/clientApp.js
delete mode 100644 nextjs-end/src/lib/firebase/firebase.js
create mode 100644 nextjs-end/src/lib/firebase/serverApp.js
create mode 100644 nextjs-start/apphosting.yaml
create mode 100644 nextjs-start/auth-service-worker.js
create mode 100644 nextjs-start/src/components/Reviews/Review.jsx
create mode 100644 nextjs-start/src/components/Reviews/ReviewSummary.jsx
create mode 100644 nextjs-start/src/components/Reviews/ReviewsList.jsx
create mode 100644 nextjs-start/src/components/Reviews/ReviewsListClient.jsx
delete mode 100644 nextjs-start/src/components/ReviewsList.jsx
create mode 100644 nextjs-start/src/lib/firebase/clientApp.js
delete mode 100644 nextjs-start/src/lib/firebase/firebase.js
create mode 100644 nextjs-start/src/lib/firebase/serverApp.js
diff --git a/nextjs-end/.gitignore b/nextjs-end/.gitignore
index 347b8c762..8fb25514e 100644
--- a/nextjs-end/.gitignore
+++ b/nextjs-end/.gitignore
@@ -1,3 +1,4 @@
lib/firebase/config.js
.next/
-.firebase/
\ No newline at end of file
+.firebase/
+node_modules/
\ No newline at end of file
diff --git a/nextjs-end/apphosting.yaml b/nextjs-end/apphosting.yaml
new file mode 100644
index 000000000..12017a252
--- /dev/null
+++ b/nextjs-end/apphosting.yaml
@@ -0,0 +1,17 @@
+env:
+ # Set this with firebase apphosting:secrets:set
+ - variable: GEMINI_API_KEY
+ secret: gemini-api-key
+ # Get these values from the Firebase console
+ - variable: NEXT_PUBLIC_FIREBASE_API_KEY
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_PROJECT_ID
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_APP_ID
+ value: TODO
\ No newline at end of file
diff --git a/nextjs-end/auth-service-worker.js b/nextjs-end/auth-service-worker.js
new file mode 100644
index 000000000..f97408692
--- /dev/null
+++ b/nextjs-end/auth-service-worker.js
@@ -0,0 +1,45 @@
+import { initializeApp } from "firebase/app";
+import { getAuth, getIdToken } from "firebase/auth";
+import { getInstallations, getToken } from "firebase/installations";
+
+// this is set during install
+let firebaseConfig;
+
+self.addEventListener('install', event => {
+ // extract firebase config from query string
+ const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');
+
+ if (!serializedFirebaseConfig) {
+ throw new Error('Firebase Config object not found in service worker query string.');
+ }
+
+ firebaseConfig = JSON.parse(serializedFirebaseConfig);
+ console.log("Service worker installed with Firebase config", firebaseConfig);
+});
+
+self.addEventListener("fetch", (event) => {
+ const { origin } = new URL(event.request.url);
+ if (origin !== self.location.origin) return;
+ event.respondWith(fetchWithFirebaseHeaders(event.request));
+});
+
+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);
+}
diff --git a/nextjs-end/functions/.gitignore b/nextjs-end/functions/.gitignore
deleted file mode 100644
index 40b878db5..000000000
--- a/nextjs-end/functions/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-node_modules/
\ No newline at end of file
diff --git a/nextjs-end/functions/index.js b/nextjs-end/functions/index.js
deleted file mode 100644
index e81477f69..000000000
--- a/nextjs-end/functions/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Import function triggers from their respective submodules:
- *
- * const {onCall} = require("firebase-functions/v2/https");
- * const {onDocumentWritten} = require("firebase-functions/v2/firestore");
- *
- * See a full list of supported triggers at https://firebase.google.com/docs/functions
- */
-
-const {onRequest} = require("firebase-functions/v2/https");
-const logger = require("firebase-functions/logger");
-
-// Create and deploy your first functions
-// https://firebase.google.com/docs/functions/get-started
-
-// exports.helloWorld = onRequest((request, response) => {
-// logger.info("Hello logs!", {structuredData: true});
-// response.send("Hello from Firebase!");
-// });
diff --git a/nextjs-end/functions/package.json b/nextjs-end/functions/package.json
deleted file mode 100644
index 392196b98..000000000
--- a/nextjs-end/functions/package.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "name": "functions",
- "description": "Cloud Functions for Firebase",
- "scripts": {
- "serve": "firebase emulators:start --only functions",
- "shell": "firebase functions:shell",
- "start": "npm run shell",
- "deploy": "firebase deploy --only functions",
- "logs": "firebase functions:log"
- },
- "engines": {
- "node": "18"
- },
- "main": "index.js",
- "dependencies": {
- "firebase-admin": "^11.8.0",
- "firebase-functions": "^4.3.1"
- },
- "devDependencies": {
- "firebase-functions-test": "^3.1.0"
- },
- "private": true
-}
diff --git a/nextjs-end/package.json b/nextjs-end/package.json
index 4de0a37e8..ba1dfd8f1 100644
--- a/nextjs-end/package.json
+++ b/nextjs-end/package.json
@@ -4,22 +4,19 @@
"private": true,
"scripts": {
"dev": "next dev",
- "build": "next build",
+ "build": "npm run build-service-worker && next build",
+ "build-service-worker": "npx esbuild auth-service-worker.js --bundle --outfile=public/auth-service-worker.js",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
- "@google-cloud/firestore": "^6.7.0",
- "firebase": "^10.3.1",
- "firebase-admin": "^11.10.1",
- "next": "13.4.10",
- "protobufjs": "^7.2.5",
- "react": "18.2.0",
- "react-dom": "18.2.0",
- "request": "^2.88.2"
- },
- "devDependencies": {
- "encoding": "^0.1.13"
+ "@google/generative-ai": "^0.10.0",
+ "firebase": "^10.11.1",
+ "firebase-admin": "^12.1.0",
+ "next": "^14.2.3",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "server-only": "^0.0.1"
},
"browser": {
"fs": false,
@@ -28,5 +25,8 @@
"child_process": false,
"net": false,
"tls": false
+ },
+ "devDependencies": {
+ "esbuild": "^0.20.2"
}
}
diff --git a/nextjs-end/src/app/actions.js b/nextjs-end/src/app/actions.js
index ec7b21af1..3bba91927 100644
--- a/nextjs-end/src/app/actions.js
+++ b/nextjs-end/src/app/actions.js
@@ -1,15 +1,14 @@
"use server";
import { addReviewToRestaurant } from "@/src/lib/firebase/firestore.js";
-import { getAuthenticatedAppForUser } from "@/src/lib/firebase/firebase";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp.js";
import { getFirestore } from "firebase/firestore";
-// This is a next.js server action, an alpha feature, so
-// use with caution
+// This is a Server Action
// 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);
+ const { firebaseServerApp } = await getAuthenticatedAppForUser();
+ const db = getFirestore(firebaseServerApp);
await addReviewToRestaurant(db, data.get("restaurantId"), {
text: data.get("text"),
diff --git a/nextjs-end/src/app/layout.js b/nextjs-end/src/app/layout.js
index 0dee17f15..c75b8fc15 100644
--- a/nextjs-end/src/app/layout.js
+++ b/nextjs-end/src/app/layout.js
@@ -1,6 +1,6 @@
import "@/src/app/styles.css";
import Header from "@/src/components/Header.jsx";
-import { getAuthenticatedAppForUser } from "@/src/lib/firebase/firebase";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp";
// Force next.js to treat this route as server-side rendered
// Without this line, during the build process, next.js will treat this route as static and build a static HTML file for it
export const dynamic = "force-dynamic";
@@ -13,15 +13,15 @@ export const metadata = {
export default async function RootLayout({ children }) {
- const { currentUser } = await getAuthenticatedAppForUser();
+ const { currentUser } = await getAuthenticatedAppForUser();
return (
-
+
);
diff --git a/nextjs-end/src/components/Restaurant.jsx b/nextjs-end/src/components/Restaurant.jsx
index 338a817c9..85848ab68 100644
--- a/nextjs-end/src/components/Restaurant.jsx
+++ b/nextjs-end/src/components/Restaurant.jsx
@@ -3,35 +3,32 @@
// This components shows one individual restaurant
// It receives data from src/app/restaurant/[id]/page.jsx
-import { React, useState, useEffect } from "react";
-import { onAuthStateChanged } from "firebase/auth";
+import { React, useState, useEffect, Suspense } from "react";
+import dynamic from 'next/dynamic'
import {
getRestaurantSnapshotById,
- getReviewsSnapshotByRestaurantId,
} from "@/src/lib/firebase/firestore.js";
-import { auth } from "@/src/lib/firebase/firebase.js";
-import {getUser} from '@/src/lib/getUser'
-import { updateRestaurantImage } from "@/src/lib/firebase/storage.js";
-import ReviewDialog from "@/src/components/ReviewDialog.jsx";
+import {useUser} from '@/src/lib/getUser'
import RestaurantDetails from "@/src/components/RestaurantDetails.jsx";
-import ReviewsList from "@/src/components/ReviewsList.jsx";
+import { updateRestaurantImage } from "@/src/lib/firebase/storage.js";
+
+const ReviewDialog = dynamic(() => import('@/src/components/ReviewDialog.jsx'));
export default function Restaurant({
id,
initialRestaurant,
- initialReviews,
initialUserId,
+ children
}) {
- const [restaurant, setRestaurant] = useState(initialRestaurant);
+ const [restaurantDetails, setRestaurantDetails] = useState(initialRestaurant);
const [isOpen, setIsOpen] = useState(false);
// The only reason this component needs to know the user ID is to associate a review with the user, and to know whether to show the review dialog
- const userId = getUser()?.uid || initialUserId;
+ const userId = useUser()?.uid || initialUserId;
const [review, setReview] = useState({
rating: 0,
text: "",
});
- const [reviews, setReviews] = useState(initialReviews);
const onChange = (value, name) => {
setReview({ ...review, [name]: value });
@@ -44,7 +41,7 @@ export default function Restaurant({
}
const imageURL = await updateRestaurantImage(id, image);
- setRestaurant({ ...restaurant, photo: imageURL });
+ setRestaurantDetails({ ...restaurant, photo: imageURL });
}
const handleClose = () => {
@@ -54,40 +51,31 @@ export default function Restaurant({
useEffect(() => {
const unsubscribeFromRestaurant = getRestaurantSnapshotById(id, (data) => {
- setRestaurant(data);
+ setRestaurantDetails(data);
});
- const unsubscribeFromReviewsSnapshot = getReviewsSnapshotByRestaurantId(
- id,
- (data) => {
- setReviews(data);
- }
- );
-
return () => {
unsubscribeFromRestaurant();
- unsubscribeFromReviewsSnapshot();
};
}, []);
return (
-
+ <>
- {children}
+ {userId && Loading...}>
-
-
+ />}
+ >
);
}
diff --git a/nextjs-end/src/components/RestaurantDetails.jsx b/nextjs-end/src/components/RestaurantDetails.jsx
index 4171af211..6f65ad8ea 100644
--- a/nextjs-end/src/components/RestaurantDetails.jsx
+++ b/nextjs-end/src/components/RestaurantDetails.jsx
@@ -9,6 +9,7 @@ const RestaurantDetails = ({
handleRestaurantImage,
setIsOpen,
isOpen,
+ children
}) => {
return (
@@ -54,6 +55,7 @@ const RestaurantDetails = ({
{restaurant.category} | {restaurant.city}
{"$".repeat(restaurant.price)}
+ {children}
diff --git a/nextjs-end/src/components/ReviewDialog.jsx b/nextjs-end/src/components/ReviewDialog.jsx
index dd1e685b1..5ab19c607 100644
--- a/nextjs-end/src/components/ReviewDialog.jsx
+++ b/nextjs-end/src/components/ReviewDialog.jsx
@@ -1,6 +1,8 @@
+'use client';
+
// This components handles the review dialog and uses a next.js feature known as Server Actions to handle the form submission
-import React from "react";
+import {useEffect, useLayoutEffect, useRef} from "react";
import RatingPicker from "@/src/components/RatingPicker.jsx";
import { handleReviewFormSubmission } from "@/src/app/actions.js";
@@ -12,9 +14,27 @@ const ReviewDialog = ({
userId,
id,
}) => {
+ const dialog = useRef();
+
+ // dialogs only render their backdrop when called with `showModal`
+ useLayoutEffect(() => {
+ if (isOpen) {
+ dialog.current.showModal();
+ } else {
+ dialog.current.close();
+ }
+
+ }, [isOpen, dialog.current]);
+
+ const handleClick = (e) => {
+ // close if clicked outside the modal
+ if (e.target === dialog.current) {
+ handleClose();
+ }
+ }
+
return (
-
);
};
diff --git a/nextjs-end/src/components/Reviews/Review.jsx b/nextjs-end/src/components/Reviews/Review.jsx
new file mode 100644
index 000000000..d52294b2f
--- /dev/null
+++ b/nextjs-end/src/components/Reviews/Review.jsx
@@ -0,0 +1,23 @@
+import renderStars from "@/src/components/Stars.jsx";
+
+
+export function Review({ rating, text, timestamp }) {
+ return (
+
+ {text}
+
+
+ );
+}
+
+export function ReviewSkeleton() {
+ return (
+
+
+ {' '}
+ );
+}
diff --git a/nextjs-end/src/components/Reviews/ReviewSummary.jsx b/nextjs-end/src/components/Reviews/ReviewSummary.jsx
new file mode 100644
index 000000000..8d3cf9316
--- /dev/null
+++ b/nextjs-end/src/components/Reviews/ReviewSummary.jsx
@@ -0,0 +1,57 @@
+const { GoogleGenerativeAI } = require("@google/generative-ai");
+import { getReviewsByRestaurantId } from "@/src/lib/firebase/firestore.js";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp";
+import { getFirestore } from "firebase/firestore";
+
+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 (
+
+
{text}
+
✨ Summarized with Gemini
+
+ );
+ } catch (e) {
+ console.error(e);
+ if (e.message.includes("403 Forbidden")) {
+ return (
+
+ This service account doesn't have permission to talk to Gemini via
+ Vertex
+
+ );
+ } else {
+ return Error contacting Gemini
;
+ }
+ }
+}
+
+export function GeminiSummarySkeleton() {
+ return (
+
+
✨ Summarizing reviews with Gemini...
+
+ );
+}
diff --git a/nextjs-end/src/components/Reviews/ReviewsList.jsx b/nextjs-end/src/components/Reviews/ReviewsList.jsx
new file mode 100644
index 000000000..939e5b97e
--- /dev/null
+++ b/nextjs-end/src/components/Reviews/ReviewsList.jsx
@@ -0,0 +1,37 @@
+// This component handles the list of reviews for a given restaurant
+
+import React from "react";
+import { getReviewsByRestaurantId } from "@/src/lib/firebase/firestore.js";
+import ReviewsListClient from "@/src/components/Reviews/ReviewsListClient";
+import { ReviewSkeleton } from "@/src/components/Reviews/Review";
+import { getFirestore } from "firebase/firestore";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp";
+
+export default async function ReviewsList({ restaurantId, userId }) {
+ const {firebaseServerApp} = await getAuthenticatedAppForUser();
+ const reviews = await getReviewsByRestaurantId(getFirestore(firebaseServerApp), restaurantId);
+
+ return (
+
+ );
+}
+
+export function ReviewsListSkeleton({ numReviews }) {
+ return (
+
+
+
+ {Array(numReviews)
+ .fill(0)
+ .map((value, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/nextjs-end/src/components/Reviews/ReviewsListClient.jsx b/nextjs-end/src/components/Reviews/ReviewsListClient.jsx
new file mode 100644
index 000000000..ce509540a
--- /dev/null
+++ b/nextjs-end/src/components/Reviews/ReviewsListClient.jsx
@@ -0,0 +1,45 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { getReviewsSnapshotByRestaurantId } from "@/src/lib/firebase/firestore.js";
+import { Review } from "@/src/components/Reviews/Review";
+
+export default function ReviewsListClient({
+ initialReviews,
+ restaurantId,
+ userId,
+}) {
+ const [reviews, setReviews] = useState(initialReviews);
+
+ useEffect(() => {
+ return getReviewsSnapshotByRestaurantId(
+ restaurantId,
+ (data) => {
+ setReviews(data);
+ }
+ );
+ }, [restaurantId]);
+ return (
+
+
+ {reviews.length > 0 ? (
+
+ {reviews.map((review) => (
+
+ ))}
+
+ ) : (
+
+ This restaurant has not been reviewed yet,{" "}
+ {!userId ? "first login and then" : ""} add your own review!
+
+ )}
+
+
+ );
+}
diff --git a/nextjs-end/src/components/ReviewsList.jsx b/nextjs-end/src/components/ReviewsList.jsx
deleted file mode 100644
index 379a0923b..000000000
--- a/nextjs-end/src/components/ReviewsList.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// This component handles the list of reviews for a given restaurant
-
-import React from "react";
-import renderStars from "@/src/components/Stars.jsx";
-
-const ReviewsList = ({ reviews, userId }) => {
- return (
-
-
- {reviews.length > 0 ? (
-
- {reviews.map(review => (
- -
-
- {renderStars(review.rating)}
-
- {review.text}
-
-
-
- ))}
-
- ) : (
-
- This restaurant has not been reviewed yet,{" "}
- {!userId ? "first login and then" : ""} add your own
- review!
-
- )}
-
-
- );
-};
-
-export default ReviewsList;
diff --git a/nextjs-end/src/lib/firebase/auth.js b/nextjs-end/src/lib/firebase/auth.js
index 44a51c17e..362984b30 100644
--- a/nextjs-end/src/lib/firebase/auth.js
+++ b/nextjs-end/src/lib/firebase/auth.js
@@ -4,7 +4,7 @@ import {
onAuthStateChanged as _onAuthStateChanged,
} from "firebase/auth";
-import { auth } from "@/src/lib/firebase/firebase";
+import { auth } from "@/src/lib/firebase/clientApp";
export function onAuthStateChanged(cb) {
return _onAuthStateChanged(auth, cb);
diff --git a/nextjs-end/src/lib/firebase/clientApp.js b/nextjs-end/src/lib/firebase/clientApp.js
new file mode 100644
index 000000000..e988ef6ed
--- /dev/null
+++ b/nextjs-end/src/lib/firebase/clientApp.js
@@ -0,0 +1,12 @@
+'use client';
+
+import { initializeApp, getApps } from "firebase/app";
+import { firebaseConfig } from "./config";
+import { getAuth } from "firebase/auth";
+import { getFirestore } from "firebase/firestore";
+import { getStorage } from "firebase/storage";
+export const firebaseApp =
+ getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
+export const auth = getAuth(firebaseApp);
+export const db = getFirestore(firebaseApp);
+export const storage = getStorage(firebaseApp);
diff --git a/nextjs-end/src/lib/firebase/config.js b/nextjs-end/src/lib/firebase/config.js
index 2f77b9eaf..00d23aefa 100644
--- a/nextjs-end/src/lib/firebase/config.js
+++ b/nextjs-end/src/lib/firebase/config.js
@@ -1,7 +1,18 @@
-export default {
- projectId: "demo-codelab-nextjs",
- appId: "demo-codelab-nextjs",
- apiKey: "demo-codelab-nextjs",
- storageBucket: "demo-codelab-nextjs.appspot.com",
- authDomain: "demo-codelab-nextjs.firebaseapp.com",
+const config = {
+ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
+ authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
+ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
+ storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
+ appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
+
+// When deployed, there are quotes that need to be stripped
+Object.keys(config).forEach((key) => {
+ const configValue = config[key] + "";
+ if (configValue.charAt(0) === '"') {
+ config[key] = configValue.substring(1, configValue.length - 1);
+ }
+});
+
+export const firebaseConfig = config;
diff --git a/nextjs-end/src/lib/firebase/firebase.js b/nextjs-end/src/lib/firebase/firebase.js
deleted file mode 100644
index 2b7ec867a..000000000
--- a/nextjs-end/src/lib/firebase/firebase.js
+++ /dev/null
@@ -1,106 +0,0 @@
-
-import { initializeApp, getApps } from "firebase/app";
-import {
- getAuth,
- connectAuthEmulator,
- signInWithCustomToken,
-} from "firebase/auth";
-import { getFirestore } from "firebase/firestore";
-import { getStorage } from "firebase/storage";
-
-export const firebaseConfig = {
- apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
- authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
- projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
- storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
- messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
- appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
-};
-
-export const firebaseApp =
- getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
-export const auth = getAuth(firebaseApp);
-export const db = getFirestore(firebaseApp);
-export const storage = getStorage(firebaseApp);
-
-
-export async function getAuthenticatedAppForUser(session = null) {
-
-
- if (typeof window !== "undefined") {
- // client
- console.log("client: ", firebaseApp);
-
- return { app: firebaseApp, user: auth.currentUser.toJSON() };
- }
-
- const { initializeApp: initializeAdminApp, getApps: getAdminApps } = await import("firebase-admin/app");
-
- const { getAuth: getAdminAuth } = await import("firebase-admin/auth");
-
- const { credential } = await import("firebase-admin");
-
- const ADMIN_APP_NAME = "firebase-frameworks";
- const adminApp =
- getAdminApps().find((it) => it.name === ADMIN_APP_NAME) ||
- initializeAdminApp({
- credential: credential.applicationDefault(),
- }, ADMIN_APP_NAME);
-
- const adminAuth = getAdminAuth(adminApp);
- const noSessionReturn = { app: null, currentUser: null };
-
-
- if (!session) {
- // if no session cookie was passed, try to get from next/headers for app router
- session = await getAppRouterSession();
-
- if (!session) return noSessionReturn;
- }
-
- const decodedIdToken = await adminAuth.verifySessionCookie(session);
-
- const app = initializeAuthenticatedApp(decodedIdToken.uid)
- const auth = getAuth(app)
-
- // handle revoked tokens
- const isRevoked = !(await adminAuth
- .verifySessionCookie(session, true)
- .catch((e) => console.error(e.message)));
- if (isRevoked) return noSessionReturn;
-
- // authenticate with custom token
- if (auth.currentUser?.uid !== decodedIdToken.uid) {
- // TODO(jamesdaniels) get custom claims
- const customToken = await adminAuth
- .createCustomToken(decodedIdToken.uid)
- .catch((e) => console.error(e.message));
-
- if (!customToken) return noSessionReturn;
-
- await signInWithCustomToken(auth, customToken);
- }
- console.log("server: ", app);
- return { app, currentUser: auth.currentUser };
-}
-
-async function getAppRouterSession() {
- // dynamically import to prevent import errors in pages router
- const { cookies } = await import("next/headers");
-
- try {
- return cookies().get("__session")?.value;
- } catch (error) {
- // cookies() throws when called from pages router
- return undefined;
- }
-}
-
-function initializeAuthenticatedApp(uid) {
- const random = Math.random().toString(36).split(".")[1];
- const appName = `authenticated-context:${uid}:${random}`;
-
- const app = initializeApp(firebaseConfig, appName);
-
- return app;
-}
diff --git a/nextjs-end/src/lib/firebase/firestore.js b/nextjs-end/src/lib/firebase/firestore.js
index 46225591e..69da95b66 100644
--- a/nextjs-end/src/lib/firebase/firestore.js
+++ b/nextjs-end/src/lib/firebase/firestore.js
@@ -13,9 +13,10 @@ import {
runTransaction,
where,
addDoc,
+ getFirestore,
} from "firebase/firestore";
-import { db } from "@/src/lib/firebase/firebase";
+import { db } from "@/src/lib/firebase/clientApp";
export async function updateRestaurantImageReference(
restaurantId,
@@ -97,7 +98,7 @@ function applyQueryFilters(q, { category, city, price, sort }) {
return q;
}
-export async function getRestaurants(filters = {}) {
+export async function getRestaurants(db = db, filters = {}) {
let q = query(collection(db, "restaurants"));
q = applyQueryFilters(q, filters);
@@ -137,7 +138,7 @@ export function getRestaurantsSnapshot(cb, filters = {}) {
return unsubscribe;
}
-export async function getRestaurantById(restaurantId) {
+export async function getRestaurantById(db, restaurantId) {
if (!restaurantId) {
console.log("Error: Invalid ID received: ", restaurantId);
return;
@@ -171,7 +172,7 @@ export function getRestaurantSnapshotById(restaurantId, cb) {
return unsubscribe;
}
-export async function getReviewsByRestaurantId(restaurantId) {
+export async function getReviewsByRestaurantId(db, restaurantId) {
if (!restaurantId) {
console.log("Error: Invalid restaurantId received: ", restaurantId);
return;
diff --git a/nextjs-end/src/lib/firebase/serverApp.js b/nextjs-end/src/lib/firebase/serverApp.js
new file mode 100644
index 000000000..5515b12a0
--- /dev/null
+++ b/nextjs-end/src/lib/firebase/serverApp.js
@@ -0,0 +1,13 @@
+// enforces that this code can only be called on the server
+// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment
+import "server-only";
+
+import { headers } from "next/headers";
+import { initializeServerApp } from "firebase/app";
+
+import { firebaseConfig } from "./config";
+import { getAuth } from "firebase/auth";
+
+export async function getAuthenticatedAppForUser() {
+ throw new Error('not implemented');
+}
\ No newline at end of file
diff --git a/nextjs-end/src/lib/firebase/storage.js b/nextjs-end/src/lib/firebase/storage.js
index f9da55645..a5c7825a2 100644
--- a/nextjs-end/src/lib/firebase/storage.js
+++ b/nextjs-end/src/lib/firebase/storage.js
@@ -1,6 +1,6 @@
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
-import { storage } from "@/src/lib/firebase/firebase";
+import { storage } from "@/src/lib/firebase/clientApp";
import { updateRestaurantImageReference } from "@/src/lib/firebase/firestore";
diff --git a/nextjs-end/src/lib/getUser.js b/nextjs-end/src/lib/getUser.js
index 86db8623f..ecba23187 100644
--- a/nextjs-end/src/lib/getUser.js
+++ b/nextjs-end/src/lib/getUser.js
@@ -1,33 +1,22 @@
-import { onAuthStateChanged } from 'firebase/auth'
-import { useEffect, useState } from 'react'
+"use client";
-import { auth } from '@/src/lib/firebase/firebase'
-import { useRouter } from 'next/navigation'
+import { onAuthStateChanged } from "firebase/auth";
+import { useEffect, useState } from "react";
-export function getUser() {
- const [user, setUser] = useState()
- const router = useRouter()
+import { auth } from "@/src/lib/firebase/clientApp.js";
+import { useRouter } from "next/navigation";
- useEffect(() => {
- const unsubscribe = onAuthStateChanged(auth, (authUser) => {
- setUser(authUser)
- })
+export function useUser() {
+ const [user, setUser] = useState();
- return () => unsubscribe()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
+ useEffect(() => {
+ const unsubscribe = onAuthStateChanged(auth, (authUser) => {
+ setUser(authUser);
+ });
- useEffect(() => {
- onAuthStateChanged(auth, (authUser) => {
- if (user === undefined) return
+ return () => unsubscribe();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- // 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
+ return user;
}
diff --git a/nextjs-start/.gitignore b/nextjs-start/.gitignore
index 347b8c762..8fb25514e 100644
--- a/nextjs-start/.gitignore
+++ b/nextjs-start/.gitignore
@@ -1,3 +1,4 @@
lib/firebase/config.js
.next/
-.firebase/
\ No newline at end of file
+.firebase/
+node_modules/
\ No newline at end of file
diff --git a/nextjs-start/apphosting.yaml b/nextjs-start/apphosting.yaml
new file mode 100644
index 000000000..f9a39181c
--- /dev/null
+++ b/nextjs-start/apphosting.yaml
@@ -0,0 +1,14 @@
+env:
+ # Get these values from the Firebase console
+ - variable: NEXT_PUBLIC_FIREBASE_API_KEY
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_PROJECT_ID
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID
+ value: TODO
+ - variable: NEXT_PUBLIC_FIREBASE_APP_ID
+ value: TODO
\ No newline at end of file
diff --git a/nextjs-start/auth-service-worker.js b/nextjs-start/auth-service-worker.js
new file mode 100644
index 000000000..a27cf9b09
--- /dev/null
+++ b/nextjs-start/auth-service-worker.js
@@ -0,0 +1,34 @@
+import { initializeApp } from "firebase/app";
+import { getAuth, getIdToken } from "firebase/auth";
+import { getInstallations, getToken } from "firebase/installations";
+
+// this is set during install
+let firebaseConfig;
+
+self.addEventListener('install', event => {
+ // extract firebase config from query string
+ const serializedFirebaseConfig = new URL(location).searchParams.get('firebaseConfig');
+
+ if (!serializedFirebaseConfig) {
+ throw new Error('Firebase Config object not found in service worker query string.');
+ }
+
+ firebaseConfig = JSON.parse(serializedFirebaseConfig);
+ console.log("Service worker installed with Firebase config", firebaseConfig);
+});
+
+self.addEventListener("fetch", (event) => {
+ const { origin } = new URL(event.request.url);
+ if (origin !== self.location.origin) return;
+ event.respondWith(fetchWithFirebaseHeaders(event.request));
+});
+
+// TODO: add Firebase Authentication headers to request
+async function fetchWithFirebaseHeaders(request) {
+ return await fetch(request);
+}
+
+// TODO: get user token
+async function getAuthIdToken(auth) {
+ throw new Error('not implemented');
+}
diff --git a/nextjs-start/firebase.json b/nextjs-start/firebase.json
index 0f0ea2b9e..2bc5e7504 100644
--- a/nextjs-start/firebase.json
+++ b/nextjs-start/firebase.json
@@ -1,40 +1,33 @@
{
- "emulators": {
- "auth": {
- "port": 9099
- },
- "functions": {
- "port": 5001
- },
- "firestore": {
- "port": 8080
- },
- "database": {
- "port": 9000
- },
- "storage": {
- "port": 9199
- },
- "ui": {
- "enabled": true
- },
- "singleProjectMode": true,
- "hosting": {
- "port": 5000
- }
- },
- "firestore": {
- "rules": "firestore.rules",
- "indexes": "firestore.indexes.json"
- },
- "hosting": {
- "source": ".",
- "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
- "frameworksBackend": {
- "region": "us-central1"
- }
- },
- "storage": {
- "rules": "storage.rules"
- }
+ "emulators": {
+ "auth": {
+ "port": 9099
+ },
+ "functions": {
+ "port": 5001
+ },
+ "firestore": {
+ "port": 8080
+ },
+ "database": {
+ "port": 9000
+ },
+ "storage": {
+ "port": 9199
+ },
+ "ui": {
+ "enabled": true
+ },
+ "singleProjectMode": true,
+ "hosting": {
+ "port": 5000
+ }
+ },
+ "firestore": {
+ "rules": "firestore.rules",
+ "indexes": "firestore.indexes.json"
+ },
+ "storage": {
+ "rules": "storage.rules"
+ }
}
diff --git a/nextjs-start/package.json b/nextjs-start/package.json
index 4de0a37e8..ba1dfd8f1 100644
--- a/nextjs-start/package.json
+++ b/nextjs-start/package.json
@@ -4,22 +4,19 @@
"private": true,
"scripts": {
"dev": "next dev",
- "build": "next build",
+ "build": "npm run build-service-worker && next build",
+ "build-service-worker": "npx esbuild auth-service-worker.js --bundle --outfile=public/auth-service-worker.js",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
- "@google-cloud/firestore": "^6.7.0",
- "firebase": "^10.3.1",
- "firebase-admin": "^11.10.1",
- "next": "13.4.10",
- "protobufjs": "^7.2.5",
- "react": "18.2.0",
- "react-dom": "18.2.0",
- "request": "^2.88.2"
- },
- "devDependencies": {
- "encoding": "^0.1.13"
+ "@google/generative-ai": "^0.10.0",
+ "firebase": "^10.11.1",
+ "firebase-admin": "^12.1.0",
+ "next": "^14.2.3",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "server-only": "^0.0.1"
},
"browser": {
"fs": false,
@@ -28,5 +25,8 @@
"child_process": false,
"net": false,
"tls": false
+ },
+ "devDependencies": {
+ "esbuild": "^0.20.2"
}
}
diff --git a/nextjs-start/src/app/actions.js b/nextjs-start/src/app/actions.js
index b65d82006..ea339e19a 100644
--- a/nextjs-start/src/app/actions.js
+++ b/nextjs-start/src/app/actions.js
@@ -1,12 +1,10 @@
"use server";
import { addReviewToRestaurant } from "@/src/lib/firebase/firestore.js";
-import { getAuthenticatedAppForUser } from "@/src/lib/firebase/firebase";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp.js";
import { getFirestore } from "firebase/firestore";
-// This is a next.js server action, an alpha feature, so
-// use with caution
+// This is a Server Action
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
-
// Replace the function below
export async function handleReviewFormSubmission(data) {}
diff --git a/nextjs-start/src/app/layout.js b/nextjs-start/src/app/layout.js
index 0dee17f15..c75b8fc15 100644
--- a/nextjs-start/src/app/layout.js
+++ b/nextjs-start/src/app/layout.js
@@ -1,6 +1,6 @@
import "@/src/app/styles.css";
import Header from "@/src/components/Header.jsx";
-import { getAuthenticatedAppForUser } from "@/src/lib/firebase/firebase";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp";
// Force next.js to treat this route as server-side rendered
// Without this line, during the build process, next.js will treat this route as static and build a static HTML file for it
export const dynamic = "force-dynamic";
@@ -13,15 +13,15 @@ export const metadata = {
export default async function RootLayout({ children }) {
- const { currentUser } = await getAuthenticatedAppForUser();
+ const { currentUser } = await getAuthenticatedAppForUser();
return (
-
+
);
diff --git a/nextjs-start/src/components/Restaurant.jsx b/nextjs-start/src/components/Restaurant.jsx
index 338a817c9..85848ab68 100644
--- a/nextjs-start/src/components/Restaurant.jsx
+++ b/nextjs-start/src/components/Restaurant.jsx
@@ -3,35 +3,32 @@
// This components shows one individual restaurant
// It receives data from src/app/restaurant/[id]/page.jsx
-import { React, useState, useEffect } from "react";
-import { onAuthStateChanged } from "firebase/auth";
+import { React, useState, useEffect, Suspense } from "react";
+import dynamic from 'next/dynamic'
import {
getRestaurantSnapshotById,
- getReviewsSnapshotByRestaurantId,
} from "@/src/lib/firebase/firestore.js";
-import { auth } from "@/src/lib/firebase/firebase.js";
-import {getUser} from '@/src/lib/getUser'
-import { updateRestaurantImage } from "@/src/lib/firebase/storage.js";
-import ReviewDialog from "@/src/components/ReviewDialog.jsx";
+import {useUser} from '@/src/lib/getUser'
import RestaurantDetails from "@/src/components/RestaurantDetails.jsx";
-import ReviewsList from "@/src/components/ReviewsList.jsx";
+import { updateRestaurantImage } from "@/src/lib/firebase/storage.js";
+
+const ReviewDialog = dynamic(() => import('@/src/components/ReviewDialog.jsx'));
export default function Restaurant({
id,
initialRestaurant,
- initialReviews,
initialUserId,
+ children
}) {
- const [restaurant, setRestaurant] = useState(initialRestaurant);
+ const [restaurantDetails, setRestaurantDetails] = useState(initialRestaurant);
const [isOpen, setIsOpen] = useState(false);
// The only reason this component needs to know the user ID is to associate a review with the user, and to know whether to show the review dialog
- const userId = getUser()?.uid || initialUserId;
+ const userId = useUser()?.uid || initialUserId;
const [review, setReview] = useState({
rating: 0,
text: "",
});
- const [reviews, setReviews] = useState(initialReviews);
const onChange = (value, name) => {
setReview({ ...review, [name]: value });
@@ -44,7 +41,7 @@ export default function Restaurant({
}
const imageURL = await updateRestaurantImage(id, image);
- setRestaurant({ ...restaurant, photo: imageURL });
+ setRestaurantDetails({ ...restaurant, photo: imageURL });
}
const handleClose = () => {
@@ -54,40 +51,31 @@ export default function Restaurant({
useEffect(() => {
const unsubscribeFromRestaurant = getRestaurantSnapshotById(id, (data) => {
- setRestaurant(data);
+ setRestaurantDetails(data);
});
- const unsubscribeFromReviewsSnapshot = getReviewsSnapshotByRestaurantId(
- id,
- (data) => {
- setReviews(data);
- }
- );
-
return () => {
unsubscribeFromRestaurant();
- unsubscribeFromReviewsSnapshot();
};
}, []);
return (
-
+ <>
- {children}
+ {userId && Loading...}>
-
-
+ />}
+ >
);
}
diff --git a/nextjs-start/src/components/RestaurantDetails.jsx b/nextjs-start/src/components/RestaurantDetails.jsx
index 4171af211..6f65ad8ea 100644
--- a/nextjs-start/src/components/RestaurantDetails.jsx
+++ b/nextjs-start/src/components/RestaurantDetails.jsx
@@ -9,6 +9,7 @@ const RestaurantDetails = ({
handleRestaurantImage,
setIsOpen,
isOpen,
+ children
}) => {
return (
@@ -54,6 +55,7 @@ const RestaurantDetails = ({
{restaurant.category} | {restaurant.city}
{"$".repeat(restaurant.price)}
+ {children}
diff --git a/nextjs-start/src/components/ReviewDialog.jsx b/nextjs-start/src/components/ReviewDialog.jsx
index dd1e685b1..5ab19c607 100644
--- a/nextjs-start/src/components/ReviewDialog.jsx
+++ b/nextjs-start/src/components/ReviewDialog.jsx
@@ -1,6 +1,8 @@
+'use client';
+
// This components handles the review dialog and uses a next.js feature known as Server Actions to handle the form submission
-import React from "react";
+import {useEffect, useLayoutEffect, useRef} from "react";
import RatingPicker from "@/src/components/RatingPicker.jsx";
import { handleReviewFormSubmission } from "@/src/app/actions.js";
@@ -12,9 +14,27 @@ const ReviewDialog = ({
userId,
id,
}) => {
+ const dialog = useRef();
+
+ // dialogs only render their backdrop when called with `showModal`
+ useLayoutEffect(() => {
+ if (isOpen) {
+ dialog.current.showModal();
+ } else {
+ dialog.current.close();
+ }
+
+ }, [isOpen, dialog.current]);
+
+ const handleClick = (e) => {
+ // close if clicked outside the modal
+ if (e.target === dialog.current) {
+ handleClose();
+ }
+ }
+
return (
-
-
+
-
);
};
diff --git a/nextjs-start/src/components/Reviews/Review.jsx b/nextjs-start/src/components/Reviews/Review.jsx
new file mode 100644
index 000000000..d52294b2f
--- /dev/null
+++ b/nextjs-start/src/components/Reviews/Review.jsx
@@ -0,0 +1,23 @@
+import renderStars from "@/src/components/Stars.jsx";
+
+
+export function Review({ rating, text, timestamp }) {
+ return (
+
+ {text}
+
+
+ );
+}
+
+export function ReviewSkeleton() {
+ return (
+
+
+ {' '}
+ );
+}
diff --git a/nextjs-start/src/components/Reviews/ReviewSummary.jsx b/nextjs-start/src/components/Reviews/ReviewSummary.jsx
new file mode 100644
index 000000000..6188fc3b9
--- /dev/null
+++ b/nextjs-start/src/components/Reviews/ReviewSummary.jsx
@@ -0,0 +1,20 @@
+const { GoogleGenerativeAI } = require("@google/generative-ai");
+import { getReviewsByRestaurantId } from "@/src/lib/firebase/firestore.js";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp";
+import { getFirestore } from "firebase/firestore";
+
+export async function GeminiSummary({ restaurantId }) {
+ return (
+
+
TODO: summarize reviews
+
+ );
+}
+
+export function GeminiSummarySkeleton() {
+ return (
+
+
✨ Summarizing reviews with Gemini...
+
+ );
+}
diff --git a/nextjs-start/src/components/Reviews/ReviewsList.jsx b/nextjs-start/src/components/Reviews/ReviewsList.jsx
new file mode 100644
index 000000000..939e5b97e
--- /dev/null
+++ b/nextjs-start/src/components/Reviews/ReviewsList.jsx
@@ -0,0 +1,37 @@
+// This component handles the list of reviews for a given restaurant
+
+import React from "react";
+import { getReviewsByRestaurantId } from "@/src/lib/firebase/firestore.js";
+import ReviewsListClient from "@/src/components/Reviews/ReviewsListClient";
+import { ReviewSkeleton } from "@/src/components/Reviews/Review";
+import { getFirestore } from "firebase/firestore";
+import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp";
+
+export default async function ReviewsList({ restaurantId, userId }) {
+ const {firebaseServerApp} = await getAuthenticatedAppForUser();
+ const reviews = await getReviewsByRestaurantId(getFirestore(firebaseServerApp), restaurantId);
+
+ return (
+
+ );
+}
+
+export function ReviewsListSkeleton({ numReviews }) {
+ return (
+
+
+
+ {Array(numReviews)
+ .fill(0)
+ .map((value, index) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/nextjs-start/src/components/Reviews/ReviewsListClient.jsx b/nextjs-start/src/components/Reviews/ReviewsListClient.jsx
new file mode 100644
index 000000000..ce509540a
--- /dev/null
+++ b/nextjs-start/src/components/Reviews/ReviewsListClient.jsx
@@ -0,0 +1,45 @@
+"use client";
+
+import React, { useState, useEffect } from "react";
+import { getReviewsSnapshotByRestaurantId } from "@/src/lib/firebase/firestore.js";
+import { Review } from "@/src/components/Reviews/Review";
+
+export default function ReviewsListClient({
+ initialReviews,
+ restaurantId,
+ userId,
+}) {
+ const [reviews, setReviews] = useState(initialReviews);
+
+ useEffect(() => {
+ return getReviewsSnapshotByRestaurantId(
+ restaurantId,
+ (data) => {
+ setReviews(data);
+ }
+ );
+ }, [restaurantId]);
+ return (
+
+
+ {reviews.length > 0 ? (
+
+ {reviews.map((review) => (
+
+ ))}
+
+ ) : (
+
+ This restaurant has not been reviewed yet,{" "}
+ {!userId ? "first login and then" : ""} add your own review!
+
+ )}
+
+
+ );
+}
diff --git a/nextjs-start/src/components/ReviewsList.jsx b/nextjs-start/src/components/ReviewsList.jsx
deleted file mode 100644
index 379a0923b..000000000
--- a/nextjs-start/src/components/ReviewsList.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-// This component handles the list of reviews for a given restaurant
-
-import React from "react";
-import renderStars from "@/src/components/Stars.jsx";
-
-const ReviewsList = ({ reviews, userId }) => {
- return (
-
-
- {reviews.length > 0 ? (
-
- {reviews.map(review => (
- -
-
- {renderStars(review.rating)}
-
- {review.text}
-
-
-
- ))}
-
- ) : (
-
- This restaurant has not been reviewed yet,{" "}
- {!userId ? "first login and then" : ""} add your own
- review!
-
- )}
-
-
- );
-};
-
-export default ReviewsList;
diff --git a/nextjs-start/src/lib/firebase/auth.js b/nextjs-start/src/lib/firebase/auth.js
index fe760bc7a..1f5781386 100644
--- a/nextjs-start/src/lib/firebase/auth.js
+++ b/nextjs-start/src/lib/firebase/auth.js
@@ -4,7 +4,7 @@ import {
onAuthStateChanged as _onAuthStateChanged,
} from "firebase/auth";
-import { auth } from "@/src/lib/firebase/firebase";
+import { auth } from "@/src/lib/firebase/clientApp";
export function onAuthStateChanged(cb) {
return () => {};
diff --git a/nextjs-start/src/lib/firebase/clientApp.js b/nextjs-start/src/lib/firebase/clientApp.js
new file mode 100644
index 000000000..e988ef6ed
--- /dev/null
+++ b/nextjs-start/src/lib/firebase/clientApp.js
@@ -0,0 +1,12 @@
+'use client';
+
+import { initializeApp, getApps } from "firebase/app";
+import { firebaseConfig } from "./config";
+import { getAuth } from "firebase/auth";
+import { getFirestore } from "firebase/firestore";
+import { getStorage } from "firebase/storage";
+export const firebaseApp =
+ getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
+export const auth = getAuth(firebaseApp);
+export const db = getFirestore(firebaseApp);
+export const storage = getStorage(firebaseApp);
diff --git a/nextjs-start/src/lib/firebase/config.js b/nextjs-start/src/lib/firebase/config.js
index 2f77b9eaf..00d23aefa 100644
--- a/nextjs-start/src/lib/firebase/config.js
+++ b/nextjs-start/src/lib/firebase/config.js
@@ -1,7 +1,18 @@
-export default {
- projectId: "demo-codelab-nextjs",
- appId: "demo-codelab-nextjs",
- apiKey: "demo-codelab-nextjs",
- storageBucket: "demo-codelab-nextjs.appspot.com",
- authDomain: "demo-codelab-nextjs.firebaseapp.com",
+const config = {
+ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
+ authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
+ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
+ storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
+ appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};
+
+// When deployed, there are quotes that need to be stripped
+Object.keys(config).forEach((key) => {
+ const configValue = config[key] + "";
+ if (configValue.charAt(0) === '"') {
+ config[key] = configValue.substring(1, configValue.length - 1);
+ }
+});
+
+export const firebaseConfig = config;
diff --git a/nextjs-start/src/lib/firebase/firebase.js b/nextjs-start/src/lib/firebase/firebase.js
deleted file mode 100644
index 8a88135bf..000000000
--- a/nextjs-start/src/lib/firebase/firebase.js
+++ /dev/null
@@ -1,104 +0,0 @@
-
-import { initializeApp, getApps } from "firebase/app";
-import {
- getAuth,
- signInWithCustomToken,
-} from "firebase/auth";
-import { getFirestore } from "firebase/firestore";
-import { getStorage } from "firebase/storage";
-
-export const firebaseConfig = {
- apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
- authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
- projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
- storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
- messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
- appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
-};
-
-export const firebaseApp =
- getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
-export const auth = getAuth(firebaseApp);
-export const db = getFirestore(firebaseApp);
-export const storage = getStorage(firebaseApp);
-
-export async function getAuthenticatedAppForUser(session = null) {
-
-
- if (typeof window !== "undefined") {
- // client
- console.log("client: ", firebaseApp);
-
- return { app: firebaseApp, user: auth.currentUser.toJSON() };
- }
-
- const { initializeApp: initializeAdminApp, getApps: getAdminApps } = await import("firebase-admin/app");
-
- const { getAuth: getAdminAuth } = await import("firebase-admin/auth");
-
- const { credential } = await import("firebase-admin");
-
- const ADMIN_APP_NAME = "firebase-frameworks";
- const adminApp =
- getAdminApps().find((it) => it.name === ADMIN_APP_NAME) ||
- initializeAdminApp({
- credential: credential.applicationDefault(),
- }, ADMIN_APP_NAME);
-
- const adminAuth = getAdminAuth(adminApp);
- const noSessionReturn = { app: null, currentUser: null };
-
-
- if (!session) {
- // if no session cookie was passed, try to get from next/headers for app router
- session = await getAppRouterSession();
-
- if (!session) return noSessionReturn;
- }
-
- const decodedIdToken = await adminAuth.verifySessionCookie(session);
-
- const app = initializeAuthenticatedApp(decodedIdToken.uid)
- const auth = getAuth(app)
-
- // handle revoked tokens
- const isRevoked = !(await adminAuth
- .verifySessionCookie(session, true)
- .catch((e) => console.error(e.message)));
- if (isRevoked) return noSessionReturn;
-
- // authenticate with custom token
- if (auth.currentUser?.uid !== decodedIdToken.uid) {
- // TODO(jamesdaniels) get custom claims
- const customToken = await adminAuth
- .createCustomToken(decodedIdToken.uid)
- .catch((e) => console.error(e.message));
-
- if (!customToken) return noSessionReturn;
-
- await signInWithCustomToken(auth, customToken);
- }
- console.log("server: ", app);
- return { app, currentUser: auth.currentUser };
-}
-
-async function getAppRouterSession() {
- // dynamically import to prevent import errors in pages router
- const { cookies } = await import("next/headers");
-
- try {
- return cookies().get("__session")?.value;
- } catch (error) {
- // cookies() throws when called from pages router
- return undefined;
- }
-}
-
-function initializeAuthenticatedApp(uid) {
- const random = Math.random().toString(36).split(".")[1];
- const appName = `authenticated-context:${uid}:${random}`;
-
- const app = initializeApp(firebaseConfig, appName);
-
- return app;
-}
diff --git a/nextjs-start/src/lib/firebase/firestore.js b/nextjs-start/src/lib/firebase/firestore.js
index f03b8fbe8..690baa0fd 100644
--- a/nextjs-start/src/lib/firebase/firestore.js
+++ b/nextjs-start/src/lib/firebase/firestore.js
@@ -13,9 +13,10 @@ import {
runTransaction,
where,
addDoc,
+ getFirestore,
} from "firebase/firestore";
-import { db } from "@/src/lib/firebase/firebase";
+import { db } from "@/src/lib/firebase/clientApp";
export async function updateRestaurantImageReference(
restaurantId,
@@ -44,7 +45,7 @@ function applyQueryFilters(q, { category, city, price, sort }) {
return;
}
-export async function getRestaurants(filters = {}) {
+export async function getRestaurants(db = db, filters = {}) {
return [];
}
@@ -52,7 +53,7 @@ export function getRestaurantsSnapshot(cb, filters = {}) {
return;
}
-export async function getRestaurantById(restaurantId) {
+export async function getRestaurantById(db, restaurantId) {
if (!restaurantId) {
console.log("Error: Invalid ID received: ", restaurantId);
return;
@@ -69,7 +70,7 @@ export function getRestaurantSnapshotById(restaurantId, cb) {
return;
}
-export async function getReviewsByRestaurantId(restaurantId) {
+export async function getReviewsByRestaurantId(db, restaurantId) {
if (!restaurantId) {
console.log("Error: Invalid restaurantId received: ", restaurantId);
return;
diff --git a/nextjs-start/src/lib/firebase/serverApp.js b/nextjs-start/src/lib/firebase/serverApp.js
new file mode 100644
index 000000000..24b268d6d
--- /dev/null
+++ b/nextjs-start/src/lib/firebase/serverApp.js
@@ -0,0 +1,27 @@
+// enforces that this code can only be called on the server
+// https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#keeping-server-only-code-out-of-the-client-environment
+import "server-only";
+
+import { headers } from "next/headers";
+import { initializeServerApp } from "firebase/app";
+
+import { firebaseConfig } from "./config";
+import { getAuth } from "firebase/auth";
+
+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 };
+}
\ No newline at end of file
diff --git a/nextjs-start/src/lib/firebase/storage.js b/nextjs-start/src/lib/firebase/storage.js
index 96b2299bb..2bb7552a9 100644
--- a/nextjs-start/src/lib/firebase/storage.js
+++ b/nextjs-start/src/lib/firebase/storage.js
@@ -1,6 +1,6 @@
import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
-import { storage } from "@/src/lib/firebase/firebase";
+import { storage } from "@/src/lib/firebase/clientApp";
import { updateRestaurantImageReference } from "@/src/lib/firebase/firestore";
diff --git a/nextjs-start/src/lib/getUser.js b/nextjs-start/src/lib/getUser.js
index 86db8623f..ecba23187 100644
--- a/nextjs-start/src/lib/getUser.js
+++ b/nextjs-start/src/lib/getUser.js
@@ -1,33 +1,22 @@
-import { onAuthStateChanged } from 'firebase/auth'
-import { useEffect, useState } from 'react'
+"use client";
-import { auth } from '@/src/lib/firebase/firebase'
-import { useRouter } from 'next/navigation'
+import { onAuthStateChanged } from "firebase/auth";
+import { useEffect, useState } from "react";
-export function getUser() {
- const [user, setUser] = useState()
- const router = useRouter()
+import { auth } from "@/src/lib/firebase/clientApp.js";
+import { useRouter } from "next/navigation";
- useEffect(() => {
- const unsubscribe = onAuthStateChanged(auth, (authUser) => {
- setUser(authUser)
- })
+export function useUser() {
+ const [user, setUser] = useState();
- return () => unsubscribe()
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
+ useEffect(() => {
+ const unsubscribe = onAuthStateChanged(auth, (authUser) => {
+ setUser(authUser);
+ });
- useEffect(() => {
- onAuthStateChanged(auth, (authUser) => {
- if (user === undefined) return
+ return () => unsubscribe();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- // 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
+ return user;
}