From cacd6b8aa4b83986cd980a3b56d82aedd3dd4bfb Mon Sep 17 00:00:00 2001 From: Cynthia Wang <44329917+HYACCCINT@users.noreply.github.com> Date: Wed, 20 Sep 2023 12:19:25 -0400 Subject: [PATCH] fix: update nextjs-start (#252) --- .DS_Store | Bin 6148 -> 6148 bytes nextjs-end/.DS_Store | Bin 0 -> 6148 bytes nextjs-end/src/lib/firebase/auth.js | 28 ----- nextjs-end/src/lib/firebase/firebase.js | 10 +- nextjs-start/.DS_Store | Bin 0 -> 6148 bytes nextjs-start/.env | 6 + nextjs-start/.gitignore | 4 +- nextjs-start/components/Restaurant.jsx | 107 ---------------- nextjs-start/firebase.json | 56 +++++++++ nextjs-start/firestore.indexes.json | 117 ++++++++++++++++++ nextjs-start/firestore.rules | 36 ++++++ nextjs-start/functions/.gitignore | 1 + nextjs-start/functions/index.js | 19 +++ nextjs-start/functions/package.json | 23 ++++ nextjs-start/lib/firebase/auth.js | 14 --- nextjs-start/lib/firebase/config-copy.js | 7 -- nextjs-start/lib/firebase/firebase.js | 18 --- nextjs-start/lib/firebase/storage.js | 11 -- nextjs-start/lib/getUser.js | 4 - nextjs-start/next.config.js | 2 +- nextjs-start/package.json | 16 ++- nextjs-start/readme.md | 23 +++- nextjs-start/src/app/actions.js | 20 ++- nextjs-start/src/app/api/route.js | 2 - nextjs-start/src/app/layout.js | 33 ++--- nextjs-start/src/app/page.js | 5 +- nextjs-start/src/app/restaurant/[id]/page.jsx | 14 ++- nextjs-start/{ => src}/components/Filters.jsx | 2 +- nextjs-start/{ => src}/components/Header.jsx | 19 +-- .../{ => src}/components/RatingPicker.jsx | 0 nextjs-start/src/components/Restaurant.jsx | 93 ++++++++++++++ .../components/RestaurantDetails.jsx | 2 +- .../components/RestaurantListings.jsx | 8 +- .../{ => src}/components/ReviewDialog.jsx | 2 +- .../{ => src}/components/ReviewsList.jsx | 2 +- nextjs-start/{ => src}/components/Stars.jsx | 0 nextjs-start/{ => src}/components/Tag.jsx | 0 nextjs-start/{ => src}/lib/fakeRestaurants.js | 4 +- nextjs-start/src/lib/firebase/auth.js | 19 +++ nextjs-start/{ => src}/lib/firebase/config.js | 0 nextjs-start/src/lib/firebase/firebase.js | 104 ++++++++++++++++ .../{ => src}/lib/firebase/firestore.js | 55 ++++---- nextjs-start/src/lib/firebase/storage.js | 30 +++++ nextjs-start/src/lib/getUser.js | 33 +++++ nextjs-start/{ => src}/lib/randomData.js | 0 nextjs-start/{ => src}/lib/utils.js | 0 nextjs-start/storage.rules | 13 ++ 47 files changed, 680 insertions(+), 282 deletions(-) create mode 100644 nextjs-end/.DS_Store create mode 100644 nextjs-start/.DS_Store create mode 100644 nextjs-start/.env delete mode 100644 nextjs-start/components/Restaurant.jsx create mode 100644 nextjs-start/firebase.json create mode 100644 nextjs-start/firestore.indexes.json create mode 100644 nextjs-start/firestore.rules create mode 100644 nextjs-start/functions/.gitignore create mode 100644 nextjs-start/functions/index.js create mode 100644 nextjs-start/functions/package.json delete mode 100644 nextjs-start/lib/firebase/auth.js delete mode 100644 nextjs-start/lib/firebase/config-copy.js delete mode 100644 nextjs-start/lib/firebase/firebase.js delete mode 100644 nextjs-start/lib/firebase/storage.js delete mode 100644 nextjs-start/lib/getUser.js delete mode 100644 nextjs-start/src/app/api/route.js rename nextjs-start/{ => src}/components/Filters.jsx (98%) rename nextjs-start/{ => src}/components/Header.jsx (76%) rename nextjs-start/{ => src}/components/RatingPicker.jsx (100%) create mode 100644 nextjs-start/src/components/Restaurant.jsx rename nextjs-start/{ => src}/components/RestaurantDetails.jsx (96%) rename nextjs-start/{ => src}/components/RestaurantListings.jsx (92%) rename nextjs-start/{ => src}/components/ReviewDialog.jsx (95%) rename nextjs-start/{ => src}/components/ReviewsList.jsx (93%) rename nextjs-start/{ => src}/components/Stars.jsx (100%) rename nextjs-start/{ => src}/components/Tag.jsx (100%) rename nextjs-start/{ => src}/lib/fakeRestaurants.js (96%) create mode 100644 nextjs-start/src/lib/firebase/auth.js rename nextjs-start/{ => src}/lib/firebase/config.js (100%) create mode 100644 nextjs-start/src/lib/firebase/firebase.js rename nextjs-start/{ => src}/lib/firebase/firestore.js (74%) create mode 100644 nextjs-start/src/lib/firebase/storage.js create mode 100644 nextjs-start/src/lib/getUser.js rename nextjs-start/{ => src}/lib/randomData.js (100%) rename nextjs-start/{ => src}/lib/utils.js (100%) create mode 100644 nextjs-start/storage.rules diff --git a/.DS_Store b/.DS_Store index ffc18c816f129916cbb6dd67ca5e68ad46eb040c..4f23a1b133b88e49f17a66a39038259de719e6c7 100644 GIT binary patch delta 81 zcmZoMXfc@JFUrcmz`)4BAi%(o#Zb(k%aF>D$B;64Bja)UwF delta 51 zcmZoMXfc@JFUrEez`)4BAi%(o1B8hTx(rzi#gi{GE@xz!+{h@g*_H7S+r$RN&Fmb1 F`2iW`4730M diff --git a/nextjs-end/.DS_Store b/nextjs-end/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 { -// const unsubscribe = onAuthStateChanged(auth, (authUser) => { -// setUser(authUser); -// }); - -// return () => unsubscribe(); -// // eslint-disable-next-line react-hooks/exhaustive-deps -// }, []); - -// useEffect(() => { -// onAuthStateChanged(auth, (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 null; -// } diff --git a/nextjs-end/src/lib/firebase/firebase.js b/nextjs-end/src/lib/firebase/firebase.js index 4fd433a91..2b7ec867a 100644 --- a/nextjs-end/src/lib/firebase/firebase.js +++ b/nextjs-end/src/lib/firebase/firebase.js @@ -5,8 +5,8 @@ import { connectAuthEmulator, signInWithCustomToken, } from "firebase/auth"; -import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"; -import { getStorage, connectStorageEmulator } from "firebase/storage"; +import { getFirestore } from "firebase/firestore"; +import { getStorage } from "firebase/storage"; export const firebaseConfig = { apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY, @@ -23,12 +23,6 @@ export const auth = getAuth(firebaseApp); export const db = getFirestore(firebaseApp); export const storage = getStorage(firebaseApp); -// For development purposes only -// connectFirestoreEmulator(db, "127.0.0.1", 8080); -// connectStorageEmulator(storage, "127.0.0.1", 9199); -// connectAuthEmulator(auth, "http://127.0.0.1:9099", { -// disableWarnings: true, -// }); export async function getAuthenticatedAppForUser(session = null) { diff --git a/nextjs-start/.DS_Store b/nextjs-start/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 { - setReview({ ...review, [name]: value }); - }; - - useEffect(() => { - const unsubscribe = onAuthStateChanged(user => { - if (user) { - setUserId(user.uid); - } else { - setUserId(""); - } - }); - return () => { - unsubscribe(); - }; - }, []); - - 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 }); - } - - const handleClose = () => { - setIsOpen(false); - setReview({ rating: 0, text: "" }); - }; - - useEffect(() => { - const unsubscribeFromRestaurant = getRestaurantSnapshotById( - id, - data => { - setRestaurant(data); - } - ); - - const unsubscribeFromReviewsSnapshot = getReviewsSnapshotByRestaurantId( - id, - data => { - setReviews(data); - } - ); - - return () => { - unsubscribeFromRestaurant(); - unsubscribeFromReviewsSnapshot(); - }; - }, []); - - return ( -
- - - -
- ); -} diff --git a/nextjs-start/firebase.json b/nextjs-start/firebase.json new file mode 100644 index 000000000..069dcbedb --- /dev/null +++ b/nextjs-start/firebase.json @@ -0,0 +1,56 @@ +{ + "emulators": { + "auth": { + "port": 9099 + }, + "functions": { + "port": 5001 + }, + "firestore": { + "port": 8080 + }, + "database": { + "port": 9000 + }, + "storage": { + "port": 9199 + }, + "ui": { + "enabled": true + }, + "singleProjectMode": true, + "hosting": { + "port": 5000 + } + }, + "functions": [ + { + "source": "functions", + "codebase": "default", + "ignore": [ + "node_modules", + ".git", + "firebase-debug.log", + "firebase-debug.*.log" + ] + } + ], + "firestore": { + "rules": "firestore.rules", + "indexes": "firestore.indexes.json" + }, + "hosting": { + "source": ".", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "frameworksBackend": { + "region": "us-central1" + } + }, + "storage": { + "rules": "storage.rules" + } +} diff --git a/nextjs-start/firestore.indexes.json b/nextjs-start/firestore.indexes.json new file mode 100644 index 000000000..0b113bda0 --- /dev/null +++ b/nextjs-start/firestore.indexes.json @@ -0,0 +1,117 @@ +{ + "indexes": [ + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "category", + "order": "ASCENDING" + }, + { + "fieldPath": "avgRating", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "category", + "order": "ASCENDING" + }, + { + "fieldPath": "numRatings", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "category", + "order": "ASCENDING" + }, + { + "fieldPath": "price", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "city", + "order": "ASCENDING" + }, + { + "fieldPath": "avgRating", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "city", + "order": "ASCENDING" + }, + { + "fieldPath": "numRatings", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "city", + "order": "ASCENDING" + }, + { + "fieldPath": "price", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "price", + "order": "ASCENDING" + }, + { + "fieldPath": "avgRating", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "restaurants", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "price", + "order": "ASCENDING" + }, + { + "fieldPath": "numRatings", + "order": "DESCENDING" + } + ] + } + ], + "fieldOverrides": [] +} diff --git a/nextjs-start/firestore.rules b/nextjs-start/firestore.rules new file mode 100644 index 000000000..f9235dc0d --- /dev/null +++ b/nextjs-start/firestore.rules @@ -0,0 +1,36 @@ +rules_version = '2'; +service cloud.firestore { + + // Determine if the value of the field "key" is the same + // before and after the request. + function unchanged(key) { + return (key in resource.data) + && (key in request.resource.data) + && (resource.data[key] == request.resource.data[key]); + } + + match /databases/{database}/documents { + // Restaurants: + // - Authenticated user can read + // - Authenticated user can create/update (for demo purposes only) + // - Updates are allowed if no fields are added and name is unchanged + // - Deletes are not allowed (default) + match /restaurants/{restaurantId} { + allow read; + allow create: if request.auth != null; + allow update: if request.auth != null + && unchanged("name"); + + // Ratings: + // - Authenticated user can read + // - Authenticated user can create if userId matches + // - Deletes and updates are not allowed (default) + match /ratings/{ratingId} { + allow read; + allow create: if request.auth != null; + allow update: if request.auth != null + && request.resource.data.userId == request.auth.uid; + } + } + } +} diff --git a/nextjs-start/functions/.gitignore b/nextjs-start/functions/.gitignore new file mode 100644 index 000000000..40b878db5 --- /dev/null +++ b/nextjs-start/functions/.gitignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/nextjs-start/functions/index.js b/nextjs-start/functions/index.js new file mode 100644 index 000000000..e81477f69 --- /dev/null +++ b/nextjs-start/functions/index.js @@ -0,0 +1,19 @@ +/** + * 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-start/functions/package.json b/nextjs-start/functions/package.json new file mode 100644 index 000000000..392196b98 --- /dev/null +++ b/nextjs-start/functions/package.json @@ -0,0 +1,23 @@ +{ + "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-start/lib/firebase/auth.js b/nextjs-start/lib/firebase/auth.js deleted file mode 100644 index db26d411b..000000000 --- a/nextjs-start/lib/firebase/auth.js +++ /dev/null @@ -1,14 +0,0 @@ -import { - GoogleAuthProvider, - signInWithPopup, - onAuthStateChanged as _onAuthStateChanged, -} from "firebase/auth"; - -import { auth } from "@/lib/firebase/firebase"; - -// Replace the following three empty functions definitions -export function onAuthStateChanged() {} - -export async function signInWithGoogle() {} - -export async function signOut() {} diff --git a/nextjs-start/lib/firebase/config-copy.js b/nextjs-start/lib/firebase/config-copy.js deleted file mode 100644 index 2f77b9eaf..000000000 --- a/nextjs-start/lib/firebase/config-copy.js +++ /dev/null @@ -1,7 +0,0 @@ -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", -}; diff --git a/nextjs-start/lib/firebase/firebase.js b/nextjs-start/lib/firebase/firebase.js deleted file mode 100644 index e0b8f515d..000000000 --- a/nextjs-start/lib/firebase/firebase.js +++ /dev/null @@ -1,18 +0,0 @@ -import firebaseConfig from "@/lib/firebase/config.js"; -import { initializeApp } from "firebase/app"; - -import { getFirestore, connectFirestoreEmulator } from "firebase/firestore"; - -import { getAuth, connectAuthEmulator } from "firebase/auth"; - -import { getStorage, connectStorageEmulator } from "firebase/storage"; - -import { getApps } from "firebase/app"; - -let app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]; - -export const db = getFirestore(app); -export const storage = getStorage(app); -export const auth = getAuth(app); - -// Connect firebase services to emulators diff --git a/nextjs-start/lib/firebase/storage.js b/nextjs-start/lib/firebase/storage.js deleted file mode 100644 index 01b9e8934..000000000 --- a/nextjs-start/lib/firebase/storage.js +++ /dev/null @@ -1,11 +0,0 @@ -import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; - -import { storage } from "@/lib/firebase/firebase"; - -import { updateRestaurantImageReference } from "@/lib/firebase/firestore"; - -// Replace this function -export async function updateRestaurantImage() {} - -// Replace this function -async function uploadImage() {} diff --git a/nextjs-start/lib/getUser.js b/nextjs-start/lib/getUser.js deleted file mode 100644 index 1e0c87338..000000000 --- a/nextjs-start/lib/getUser.js +++ /dev/null @@ -1,4 +0,0 @@ -import { cookies } from "next/headers"; - -// Replace this function -export default function getUser() {} diff --git a/nextjs-start/next.config.js b/nextjs-start/next.config.js index 5f2ec78f9..b03e6c1d7 100644 --- a/nextjs-start/next.config.js +++ b/nextjs-start/next.config.js @@ -2,7 +2,7 @@ const nextConfig = { experimental: { serverActions: true, - }, + } }; module.exports = nextConfig; diff --git a/nextjs-start/package.json b/nextjs-start/package.json index 40a53e539..4de0a37e8 100644 --- a/nextjs-start/package.json +++ b/nextjs-start/package.json @@ -9,12 +9,24 @@ "lint": "next lint" }, "dependencies": { - "firebase": "^10.0.0", + "@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" + "react-dom": "18.2.0", + "request": "^2.88.2" }, "devDependencies": { "encoding": "^0.1.13" + }, + "browser": { + "fs": false, + "os": false, + "path": false, + "child_process": false, + "net": false, + "tls": false } } diff --git a/nextjs-start/readme.md b/nextjs-start/readme.md index 9fb429bce..08d1b9eea 100644 --- a/nextjs-start/readme.md +++ b/nextjs-start/readme.md @@ -1,14 +1,27 @@ -### Starter code: Friendly Eats with Next.js + Firebase +### Friendly Eats with Next.js + Firebase -The codelab has the full instructions for how to run this project, but as a quick start: +The codelab has the full instructions, but as a quick start, you can do this. + +#### Run the application 1. In your terminal, run: ```sh -npm install +firebase emulators:start --project demo-codelab-nextjs +``` + +2. Copy the file `lib/firebase/config-copy.js` to `lib/firebase/config.js` and fill in the values from the Firebase console. + +3. In a new terminal tab/window, run: + +```sh +npm i npm run dev ``` -2. Open [http://localhost:3000](http://localhost:3000) in your browser. +4. In your browser, open the URL: `http://localhost:3000` + +#### Use the application -While the page should load, various features are not yet implemented as this is the starter code. The codelab explains the missing features and how to implement them. +1. While on `http://localhost:3000/` within your browser, click the "Sign in" button in the top right corner and sign in. +2. In the dropdown menu in the top right menu, select "Add sample restaurants". diff --git a/nextjs-start/src/app/actions.js b/nextjs-start/src/app/actions.js index 974ec54f7..ec7b21af1 100644 --- a/nextjs-start/src/app/actions.js +++ b/nextjs-start/src/app/actions.js @@ -1,5 +1,21 @@ "use server"; -import { addReviewToRestaurant } from "@/lib/firebase/firestore.js"; +import { addReviewToRestaurant } from "@/src/lib/firebase/firestore.js"; +import { getAuthenticatedAppForUser } from "@/src/lib/firebase/firebase"; +import { getFirestore } from "firebase/firestore"; -export async function handleReviewFormSubmission() {} +// This is a next.js server action, 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"), + }); +} diff --git a/nextjs-start/src/app/api/route.js b/nextjs-start/src/app/api/route.js deleted file mode 100644 index 9f01e810f..000000000 --- a/nextjs-start/src/app/api/route.js +++ /dev/null @@ -1,2 +0,0 @@ -// Replace this POST function -export async function POST() {} diff --git a/nextjs-start/src/app/layout.js b/nextjs-start/src/app/layout.js index b8d4dd541..0dee17f15 100644 --- a/nextjs-start/src/app/layout.js +++ b/nextjs-start/src/app/layout.js @@ -1,25 +1,28 @@ import "@/src/app/styles.css"; -import Header from "@/components/Header.jsx"; -import getUser from "@/lib/getUser.js"; - +import Header from "@/src/components/Header.jsx"; +import { getAuthenticatedAppForUser } from "@/src/lib/firebase/firebase"; // 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"; export const metadata = { - title: "FriendlyEats", - description: - "FriendlyEats is a restaurant review website built with Next.js and Firebase.", + title: "FriendlyEats", + description: + "FriendlyEats is a restaurant review website built with Next.js and Firebase.", }; -export default function RootLayout({ children }) { - return ( - - -
- {children} - - - ); +export default async function RootLayout({ children }) { + const { currentUser } = await getAuthenticatedAppForUser(); + return ( + + + +
+ +
{children}
+ + + + ); } diff --git a/nextjs-start/src/app/page.js b/nextjs-start/src/app/page.js index f902937bf..558891746 100644 --- a/nextjs-start/src/app/page.js +++ b/nextjs-start/src/app/page.js @@ -1,5 +1,5 @@ -import RestaurantListings from "@/components/RestaurantListings.jsx"; -import { getRestaurants } from "@/lib/firebase/firestore.js"; +import RestaurantListings from "@/src/components/RestaurantListings.jsx"; +import { getRestaurants } from "@/src/lib/firebase/firestore.js"; // 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 @@ -13,7 +13,6 @@ export default async function Home({ searchParams }) { // Using seachParams which Next.js provides, allows the filtering to happen on the server-side, for example: // ?city=London&category=Indian&sort=Review const restaurants = await getRestaurants(searchParams); - return (
@@ -20,7 +26,7 @@ export default async function Home({ params }) { id={params.id} initialRestaurant={restaurant} initialReviews={reviews} - initialUser={getUser()} + initialUserId={currentUser?.uid || ""} />
); diff --git a/nextjs-start/components/Filters.jsx b/nextjs-start/src/components/Filters.jsx similarity index 98% rename from nextjs-start/components/Filters.jsx rename to nextjs-start/src/components/Filters.jsx index e07b09e42..4e547fa05 100644 --- a/nextjs-start/components/Filters.jsx +++ b/nextjs-start/src/components/Filters.jsx @@ -1,6 +1,6 @@ // The filters shown on the restaurant listings page -import Tag from "@/components/Tag.jsx"; +import Tag from "@/src/components/Tag.jsx"; function FilterSelect({ label, options, value, onChange, name, icon }) { return ( diff --git a/nextjs-start/components/Header.jsx b/nextjs-start/src/components/Header.jsx similarity index 76% rename from nextjs-start/components/Header.jsx rename to nextjs-start/src/components/Header.jsx index 9e6d8bc93..30293adbd 100644 --- a/nextjs-start/components/Header.jsx +++ b/nextjs-start/src/components/Header.jsx @@ -1,20 +1,21 @@ -"use client"; - +'use client' import React, { useState, useEffect } from "react"; import Link from "next/link"; import { signInWithGoogle, signOut, - onAuthStateChanged, -} from "@/lib/firebase/auth.js"; -import { addFakeRestaurantsAndReviews } from "@/lib/firebase/firestore.js"; + onAuthStateChanged +} from "@/src/lib/firebase/auth.js"; +import { addFakeRestaurantsAndReviews } from "@/src/lib/firebase/firestore.js"; +import { useRouter } from "next/navigation"; -async function handleUserSession() {} +function useUserSession(initialUser) { + return; +} -function useUserSession() {} +export default function Header({initialUser}) { -export default function Header({ initialUser }) { - const user = useUserSession(initialUser); + const user = useUserSession(initialUser) ; const handleSignOut = event => { event.preventDefault(); diff --git a/nextjs-start/components/RatingPicker.jsx b/nextjs-start/src/components/RatingPicker.jsx similarity index 100% rename from nextjs-start/components/RatingPicker.jsx rename to nextjs-start/src/components/RatingPicker.jsx diff --git a/nextjs-start/src/components/Restaurant.jsx b/nextjs-start/src/components/Restaurant.jsx new file mode 100644 index 000000000..338a817c9 --- /dev/null +++ b/nextjs-start/src/components/Restaurant.jsx @@ -0,0 +1,93 @@ +"use client"; + +// 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 { + 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 RestaurantDetails from "@/src/components/RestaurantDetails.jsx"; +import ReviewsList from "@/src/components/ReviewsList.jsx"; + +export default function Restaurant({ + id, + initialRestaurant, + initialReviews, + initialUserId, +}) { + const [restaurant, setRestaurant] = 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 [review, setReview] = useState({ + rating: 0, + text: "", + }); + const [reviews, setReviews] = useState(initialReviews); + + const onChange = (value, name) => { + setReview({ ...review, [name]: value }); + }; + + 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 }); + } + + const handleClose = () => { + setIsOpen(false); + setReview({ rating: 0, text: "" }); + }; + + useEffect(() => { + const unsubscribeFromRestaurant = getRestaurantSnapshotById(id, (data) => { + setRestaurant(data); + }); + + const unsubscribeFromReviewsSnapshot = getReviewsSnapshotByRestaurantId( + id, + (data) => { + setReviews(data); + } + ); + + return () => { + unsubscribeFromRestaurant(); + unsubscribeFromReviewsSnapshot(); + }; + }, []); + + return ( +
+ + + +
+ ); +} diff --git a/nextjs-start/components/RestaurantDetails.jsx b/nextjs-start/src/components/RestaurantDetails.jsx similarity index 96% rename from nextjs-start/components/RestaurantDetails.jsx rename to nextjs-start/src/components/RestaurantDetails.jsx index 42d63c311..4171af211 100644 --- a/nextjs-start/components/RestaurantDetails.jsx +++ b/nextjs-start/src/components/RestaurantDetails.jsx @@ -1,7 +1,7 @@ // This component shows restaurant metadata, and offers some actions to the user like uploading a new restaurant image, and adding a review. import React from "react"; -import renderStars from "@/components/Stars.jsx"; +import renderStars from "@/src/components/Stars.jsx"; const RestaurantDetails = ({ restaurant, diff --git a/nextjs-start/components/RestaurantListings.jsx b/nextjs-start/src/components/RestaurantListings.jsx similarity index 92% rename from nextjs-start/components/RestaurantListings.jsx rename to nextjs-start/src/components/RestaurantListings.jsx index f2142ef9c..4252640e3 100644 --- a/nextjs-start/components/RestaurantListings.jsx +++ b/nextjs-start/src/components/RestaurantListings.jsx @@ -6,9 +6,9 @@ import Link from "next/link"; import { React, useState, useEffect } from "react"; import { useRouter } from "next/navigation"; -import renderStars from "@/components/Stars.jsx"; -import { getRestaurantsSnapshot } from "@/lib/firebase/firestore.js"; -import Filters from "@/components/Filters.jsx"; +import renderStars from "@/src/components/Stars.jsx"; +import { getRestaurantsSnapshot } from "@/src/lib/firebase/firestore.js"; +import Filters from "@/src/components/Filters.jsx"; const RestaurantItem = ({ restaurant }) => (
  • @@ -90,7 +90,7 @@ export default function RestaurantListings({
      - {restaurants?.map(restaurant => ( + {restaurants.map(restaurant => ( { return ( diff --git a/nextjs-start/components/Stars.jsx b/nextjs-start/src/components/Stars.jsx similarity index 100% rename from nextjs-start/components/Stars.jsx rename to nextjs-start/src/components/Stars.jsx diff --git a/nextjs-start/components/Tag.jsx b/nextjs-start/src/components/Tag.jsx similarity index 100% rename from nextjs-start/components/Tag.jsx rename to nextjs-start/src/components/Tag.jsx diff --git a/nextjs-start/lib/fakeRestaurants.js b/nextjs-start/src/lib/fakeRestaurants.js similarity index 96% rename from nextjs-start/lib/fakeRestaurants.js rename to nextjs-start/src/lib/fakeRestaurants.js index 158293609..621516fdb 100644 --- a/nextjs-start/lib/fakeRestaurants.js +++ b/nextjs-start/src/lib/fakeRestaurants.js @@ -2,8 +2,8 @@ import { randomNumberBetween, getRandomDateAfter, getRandomDateBefore, -} from "@/lib/utils.js"; -import { randomData } from "@/lib/randomData.js"; +} from "@/src/lib/utils.js"; +import { randomData } from "@/src/lib/randomData.js"; import { Timestamp } from "firebase/firestore"; diff --git a/nextjs-start/src/lib/firebase/auth.js b/nextjs-start/src/lib/firebase/auth.js new file mode 100644 index 000000000..fe760bc7a --- /dev/null +++ b/nextjs-start/src/lib/firebase/auth.js @@ -0,0 +1,19 @@ +import { + GoogleAuthProvider, + signInWithPopup, + onAuthStateChanged as _onAuthStateChanged, +} from "firebase/auth"; + +import { auth } from "@/src/lib/firebase/firebase"; + +export function onAuthStateChanged(cb) { + return () => {}; +} + +export async function signInWithGoogle() { + return; +} + +export async function signOut() { + return; +} diff --git a/nextjs-start/lib/firebase/config.js b/nextjs-start/src/lib/firebase/config.js similarity index 100% rename from nextjs-start/lib/firebase/config.js rename to nextjs-start/src/lib/firebase/config.js diff --git a/nextjs-start/src/lib/firebase/firebase.js b/nextjs-start/src/lib/firebase/firebase.js new file mode 100644 index 000000000..8a88135bf --- /dev/null +++ b/nextjs-start/src/lib/firebase/firebase.js @@ -0,0 +1,104 @@ + +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/lib/firebase/firestore.js b/nextjs-start/src/lib/firebase/firestore.js similarity index 74% rename from nextjs-start/lib/firebase/firestore.js rename to nextjs-start/src/lib/firebase/firestore.js index ddaf68225..f03b8fbe8 100644 --- a/nextjs-start/lib/firebase/firestore.js +++ b/nextjs-start/src/lib/firebase/firestore.js @@ -1,4 +1,4 @@ -import { generateFakeRestaurantsAndReviews } from "@/lib/fakeRestaurants.js"; +import { generateFakeRestaurantsAndReviews } from "@/src/lib/fakeRestaurants.js"; import { collection, @@ -15,7 +15,7 @@ import { addDoc, } from "firebase/firestore"; -import { db } from "@/lib/firebase/firebase"; +import { db } from "@/src/lib/firebase/firebase"; export async function updateRestaurantImageReference( restaurantId, @@ -27,19 +27,30 @@ export async function updateRestaurantImageReference( } } -const updateWithRating = async () => {}; - -// Replace this function -export async function addReviewToRestaurant() {} +const updateWithRating = async ( + transaction, + docRef, + newRatingDocument, + review +) => { + return; +}; + +export async function addReviewToRestaurant(db, restaurantId, review) { + return; +} -// Replace this function -function applyQueryFilters() {} +function applyQueryFilters(q, { category, city, price, sort }) { + return; +} -// Replace this function -export async function getRestaurants() {} +export async function getRestaurants(filters = {}) { + return []; +} -// Replace this function -export function getRestaurantsSnapshot() {} +export function getRestaurantsSnapshot(cb, filters = {}) { + return; +} export async function getRestaurantById(restaurantId) { if (!restaurantId) { @@ -55,24 +66,7 @@ export async function getRestaurantById(restaurantId) { } export function getRestaurantSnapshotById(restaurantId, cb) { - if (!restaurantId) { - console.log("Error: Invalid ID received: ", restaurantId); - return; - } - - if (typeof cb !== "function") { - console.log("Error: The callback parameter is not a function"); - return; - } - - const docRef = doc(db, "restaurants", restaurantId); - const unsubscribe = onSnapshot(docRef, docSnap => { - cb({ - ...docSnap.data(), - timestamp: docSnap.data().timestamp.toDate(), - }); - }); - return unsubscribe; + return; } export async function getReviewsByRestaurantId(restaurantId) { @@ -123,7 +117,6 @@ export function getReviewsSnapshotByRestaurantId(restaurantId, cb) { export async function addFakeRestaurantsAndReviews() { const data = await generateFakeRestaurantsAndReviews(); - for (const { restaurantData, ratingsData } of data) { try { const docRef = await addDoc( diff --git a/nextjs-start/src/lib/firebase/storage.js b/nextjs-start/src/lib/firebase/storage.js new file mode 100644 index 000000000..f9da55645 --- /dev/null +++ b/nextjs-start/src/lib/firebase/storage.js @@ -0,0 +1,30 @@ +import { ref, uploadBytesResumable, getDownloadURL } from "firebase/storage"; + +import { storage } from "@/src/lib/firebase/firebase"; + +import { updateRestaurantImageReference } from "@/src/lib/firebase/firestore"; + +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); +} diff --git a/nextjs-start/src/lib/getUser.js b/nextjs-start/src/lib/getUser.js new file mode 100644 index 000000000..86db8623f --- /dev/null +++ b/nextjs-start/src/lib/getUser.js @@ -0,0 +1,33 @@ +import { onAuthStateChanged } from 'firebase/auth' +import { useEffect, useState } from 'react' + +import { auth } from '@/src/lib/firebase/firebase' +import { useRouter } from 'next/navigation' + +export function getUser() { + const [user, setUser] = useState() + const router = useRouter() + + useEffect(() => { + const unsubscribe = onAuthStateChanged(auth, (authUser) => { + setUser(authUser) + }) + + return () => unsubscribe() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + onAuthStateChanged(auth, (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 +} diff --git a/nextjs-start/lib/randomData.js b/nextjs-start/src/lib/randomData.js similarity index 100% rename from nextjs-start/lib/randomData.js rename to nextjs-start/src/lib/randomData.js diff --git a/nextjs-start/lib/utils.js b/nextjs-start/src/lib/utils.js similarity index 100% rename from nextjs-start/lib/utils.js rename to nextjs-start/src/lib/utils.js diff --git a/nextjs-start/storage.rules b/nextjs-start/storage.rules new file mode 100644 index 000000000..9a4d82102 --- /dev/null +++ b/nextjs-start/storage.rules @@ -0,0 +1,13 @@ +rules_version = '2'; + +// Craft rules based on data in your Firestore database +// allow write: if firestore.get( +// /databases/(default)/documents/users/$(request.auth.uid)).data.isAdmin; +service firebase.storage { + match /b/{bucket}/o { + match /{allPaths=**} { + allow read; + allow write: if request.auth.uid != null; + } + } +}