diff --git a/nextjs-end/.prettierrc.json b/nextjs-end/.prettierrc.json index 48c545c5..be084604 100644 --- a/nextjs-end/.prettierrc.json +++ b/nextjs-end/.prettierrc.json @@ -1,8 +1,5 @@ { "trailingComma": "all", "semi": true, - "tabWidth": 2, - "singleQuote": true, - "jsxSingleQuote": true, - "plugins": [] + "tabWidth": 2 } diff --git a/nextjs-end/auth-service-worker.js b/nextjs-end/auth-service-worker.js index b24d82cc..c7945c70 100644 --- a/nextjs-end/auth-service-worker.js +++ b/nextjs-end/auth-service-worker.js @@ -1,13 +1,13 @@ -import { initializeApp } from 'firebase/app'; -import { getAuth, getIdToken, onAuthStateChanged } from 'firebase/auth'; +import { initializeApp } from "firebase/app"; +import { getAuth, getIdToken, onAuthStateChanged } from "firebase/auth"; // extract firebase config from query string const serializedFirebaseConfig = new URLSearchParams(self.location.search).get( - 'firebaseConfig', + "firebaseConfig", ); if (!serializedFirebaseConfig) { throw new Error( - 'Firebase Config object not found in service worker query string.', + "Firebase Config object not found in service worker query string.", ); } @@ -16,29 +16,29 @@ const firebaseConfig = JSON.parse(serializedFirebaseConfig); const app = initializeApp(firebaseConfig); const auth = getAuth(app); -self.addEventListener('install', () => { - console.log('Service worker installed with Firebase config', firebaseConfig); +self.addEventListener("install", () => { + console.log("Service worker installed with Firebase config", firebaseConfig); self.skipWaiting(); }); -self.addEventListener('activate', (event) => { +self.addEventListener("activate", (event) => { event.waitUntil(self.clients.claim()); }); -self.addEventListener('fetch', (event) => { +self.addEventListener("fetch", (event) => { const { origin, pathname } = new URL(event.request.url); if (origin !== self.location.origin) return; // Use a magic url to ensure that auth state is in sync between // the client and the sw, this helps with actions such as router.refresh(); - if (pathname.startsWith('/__/auth/wait/')) { - const uid = pathname.split('/').at(-1); + if (pathname.startsWith("/__/auth/wait/")) { + const uid = pathname.split("/").at(-1); event.respondWith(waitForMatchingUid(uid)); return; } - if (pathname.startsWith('/_next/')) return; + if (pathname.startsWith("/_next/")) return; // Don't add headers to non-get requests or those with an extension—this // helps with css, images, fonts, json, etc. - if (event.request.method === 'GET' && pathname.includes('.')) return; + if (event.request.method === "GET" && pathname.includes(".")) return; event.respondWith(fetchWithFirebaseHeaders(event.request)); }); @@ -46,20 +46,20 @@ async function fetchWithFirebaseHeaders(request) { const authIdToken = await getAuthIdToken(); if (authIdToken) { const headers = new Headers(request.headers); - headers.append('Authorization', `Bearer ${authIdToken}`); + headers.append("Authorization", `Bearer ${authIdToken}`); request = new Request(request, { headers }); } return await fetch(request).catch((reason) => { console.error(reason); - return new Response('Fail.', { + return new Response("Fail.", { status: 500, - headers: { 'content-type': 'text/html' }, + headers: { "content-type": "text/html" }, }); }); } async function waitForMatchingUid(_uid) { - const uid = _uid === 'undefined' ? undefined : _uid; + const uid = _uid === "undefined" ? undefined : _uid; await auth.authStateReady(); await new Promise((resolve) => { const unsubscribe = onAuthStateChanged(auth, (user) => { @@ -71,7 +71,7 @@ async function waitForMatchingUid(_uid) { }); return new Response(undefined, { status: 200, - headers: { 'cache-control': 'no-store' }, + headers: { "cache-control": "no-store" }, }); } diff --git a/nextjs-end/src/app/actions.js b/nextjs-end/src/app/actions.js index dee55aee..79cc8a61 100644 --- a/nextjs-end/src/app/actions.js +++ b/nextjs-end/src/app/actions.js @@ -1,8 +1,8 @@ -'use server'; +"use server"; -import { addReviewToRestaurant } from '@/src/lib/firebase/firestore.js'; -import { getAuthenticatedAppForUser } from '@/src/lib/firebase/serverApp.js'; -import { getFirestore } from 'firebase/firestore'; +import { addReviewToRestaurant } from "@/src/lib/firebase/firestore.js"; +import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp.js"; +import { getFirestore } from "firebase/firestore"; // This is a Server Action // https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions @@ -10,11 +10,11 @@ export async function handleReviewFormSubmission(data) { const { firebaseServerApp } = await getAuthenticatedAppForUser(); const db = getFirestore(firebaseServerApp); - await addReviewToRestaurant(db, data.get('restaurantId'), { - text: data.get('text'), - rating: data.get('rating'), + 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'), + userId: data.get("userId"), }); } diff --git a/nextjs-end/src/app/layout.js b/nextjs-end/src/app/layout.js index 6604e904..dad6ea92 100644 --- a/nextjs-end/src/app/layout.js +++ b/nextjs-end/src/app/layout.js @@ -1,20 +1,20 @@ -import '@/src/app/styles.css'; -import Header from '@/src/components/Header.jsx'; -import { getAuthenticatedAppForUser } from '@/src/lib/firebase/serverApp'; +import "@/src/app/styles.css"; +import Header from "@/src/components/Header.jsx"; +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'; +export const dynamic = "force-dynamic"; export const metadata = { - title: 'FriendlyEats', + title: "FriendlyEats", description: - 'FriendlyEats is a restaurant review website built with Next.js and Firebase.', + "FriendlyEats is a restaurant review website built with Next.js and Firebase.", }; export default async function RootLayout({ children }) { const { currentUser } = await getAuthenticatedAppForUser(); return ( - +
diff --git a/nextjs-end/src/app/page.js b/nextjs-end/src/app/page.js index f092e885..e07647f3 100644 --- a/nextjs-end/src/app/page.js +++ b/nextjs-end/src/app/page.js @@ -1,12 +1,12 @@ -import RestaurantListings from '@/src/components/RestaurantListings.jsx'; -import { getRestaurants } from '@/src/lib/firebase/firestore.js'; -import { getAuthenticatedAppForUser } from '@/src/lib/firebase/serverApp.js'; -import { getFirestore } from 'firebase/firestore'; +import RestaurantListings from "@/src/components/RestaurantListings.jsx"; +import { getRestaurants } from "@/src/lib/firebase/firestore.js"; +import { getAuthenticatedAppForUser } from "@/src/lib/firebase/serverApp.js"; +import { getFirestore } from "firebase/firestore"; // 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 dynamic = "force-dynamic"; // This line also forces this route to be server-side rendered // export const revalidate = 0; @@ -20,7 +20,7 @@ export default async function Home({ searchParams }) { searchParams, ); return ( -
+
{ diff --git a/nextjs-end/src/app/restaurant/[id]/page.jsx b/nextjs-end/src/app/restaurant/[id]/page.jsx index 0ab46dfc..d8e16acf 100644 --- a/nextjs-end/src/app/restaurant/[id]/page.jsx +++ b/nextjs-end/src/app/restaurant/[id]/page.jsx @@ -1,18 +1,18 @@ -import Restaurant from '@/src/components/Restaurant.jsx'; -import { Suspense } from 'react'; -import { getRestaurantById } from '@/src/lib/firebase/firestore.js'; +import Restaurant from "@/src/components/Restaurant.jsx"; +import { Suspense } from "react"; +import { getRestaurantById } from "@/src/lib/firebase/firestore.js"; import { getAuthenticatedAppForUser, getAuthenticatedAppForUser as getUser, -} from '@/src/lib/firebase/serverApp.js'; +} from "@/src/lib/firebase/serverApp.js"; import ReviewsList, { ReviewsListSkeleton, -} from '@/src/components/Reviews/ReviewsList'; +} from "@/src/components/Reviews/ReviewsList"; import { GeminiSummary, GeminiSummarySkeleton, -} from '@/src/components/Reviews/ReviewSummary'; -import { getFirestore } from 'firebase/firestore'; +} from "@/src/components/Reviews/ReviewSummary"; +import { getFirestore } from "firebase/firestore"; export default async function Home({ params }) { const { currentUser } = await getUser(); @@ -23,11 +23,11 @@ export default async function Home({ params }) { ); return ( -
+
}> @@ -36,7 +36,7 @@ export default async function Home({ params }) { } > - +
); diff --git a/nextjs-end/src/app/styles.css b/nextjs-end/src/app/styles.css index d4036677..b9584b99 100644 --- a/nextjs-end/src/app/styles.css +++ b/nextjs-end/src/app/styles.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,500;0,700;1,800&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap'); +@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,500;0,700;1,800&family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); * { box-sizing: border-box; @@ -8,7 +8,7 @@ body { font-family: - 'Roboto', + "Roboto", ui-sans-serif, system-ui, -apple-system; @@ -289,9 +289,9 @@ a { } .restaurant__review_summary { - max-width: '50vw'; - height: '75px'; - padding-top: '10px'; + max-width: "50vw"; + height: "75px"; + padding-top: "10px"; } .img__section { @@ -419,7 +419,7 @@ a { } .radio-label:before { - content: '★'; + content: "★"; display: inline-block; font-size: 32px; } @@ -454,7 +454,7 @@ a { .average-rating::before { --percent: calc(4.3 / 5 * 100%); - content: '★★★★★'; + content: "★★★★★"; position: absolute; top: 0; left: 0; diff --git a/nextjs-end/src/components/Filters.jsx b/nextjs-end/src/components/Filters.jsx index 985dcca2..515e7961 100644 --- a/nextjs-end/src/components/Filters.jsx +++ b/nextjs-end/src/components/Filters.jsx @@ -1,6 +1,6 @@ // The filters shown on the restaurant listings page -import Tag from '@/src/components/Tag.jsx'; +import Tag from "@/src/components/Tag.jsx"; function FilterSelect({ label, options, value, onChange, name, icon }) { return ( @@ -11,7 +11,7 @@ function FilterSelect({ label, options, value, onChange, name, icon }) { @@ -33,104 +33,104 @@ export default function Filters({ filters, setFilters }) { }; return ( -
-
+
+
- filter + filter

Restaurants

-

Sorted by {filters.sort || 'Rating'}

+

Sorted by {filters.sort || "Rating"}

{ event.preventDefault(); - event.target.parentNode.removeAttribute('open'); + event.target.parentNode.removeAttribute("open"); }} > handleSelectionChange(event, 'category')} - name='category' - icon='/food.svg' + onChange={(event) => handleSelectionChange(event, "category")} + name="category" + icon="/food.svg" /> handleSelectionChange(event, 'city')} - name='city' - icon='/location.svg' + onChange={(event) => handleSelectionChange(event, "city")} + name="city" + icon="/location.svg" /> handleSelectionChange(event, 'price')} - name='price' - icon='/price.svg' + onChange={(event) => handleSelectionChange(event, "price")} + name="price" + icon="/price.svg" /> handleSelectionChange(event, 'sort')} - name='sort' - icon='/sortBy.svg' + onChange={(event) => handleSelectionChange(event, "sort")} + name="sort" + icon="/sortBy.svg" />
- @@ -138,12 +138,12 @@ export default function Filters({ filters, setFilters }) {
-
+
{Object.entries(filters).map(([type, value]) => { // The main filter bar already specifies what // sorting is being used. So skip showing the // sorting as a 'tag' - if (type == 'sort' || value == '') { + if (type == "sort" || value == "") { return null; } return ( diff --git a/nextjs-end/src/components/Header.jsx b/nextjs-end/src/components/Header.jsx index 880f414f..76dd4c04 100644 --- a/nextjs-end/src/components/Header.jsx +++ b/nextjs-end/src/components/Header.jsx @@ -1,14 +1,14 @@ -'use client'; -import React, { useState, useEffect } from 'react'; -import Link from 'next/link'; +"use client"; +import React, { useState, useEffect } from "react"; +import Link from "next/link"; import { signInWithGoogle, signOut, onAuthStateChanged, -} from '@/src/lib/firebase/auth.js'; -import { addFakeRestaurantsAndReviews } from '@/src/lib/firebase/firestore.js'; -import { useRouter } from 'next/navigation'; -import { firebaseConfig } from '@/src/lib/firebase/config'; +} from "@/src/lib/firebase/auth.js"; +import { addFakeRestaurantsAndReviews } from "@/src/lib/firebase/firestore.js"; +import { useRouter } from "next/navigation"; +import { firebaseConfig } from "@/src/lib/firebase/config"; function useUserSession(initialUser) { // The initialUser comes from the server via a server component @@ -18,16 +18,16 @@ function useUserSession(initialUser) { // Register the service worker that sends auth state back to server // The service worker is built with npm run build-service-worker useEffect(() => { - if ('serviceWorker' in navigator) { + if ("serviceWorker" in navigator) { const serializedFirebaseConfig = encodeURIComponent( JSON.stringify(firebaseConfig), ); const serviceWorkerUrl = `/auth-service-worker.js?firebaseConfig=${serializedFirebaseConfig}`; navigator.serviceWorker - .register(serviceWorkerUrl, { scope: '/', updateViaCache: 'none' }) + .register(serviceWorkerUrl, { scope: "/", updateViaCache: "none" }) .then((registration) => { - console.log('scope is: ', registration.scope); + console.log("scope is: ", registration.scope); registration.update(); }); } @@ -38,9 +38,9 @@ function useUserSession(initialUser) { if (user?.uid === authUser?.uid) { return; } - if ('serviceWorker' in navigator) { + if ("serviceWorker" in navigator) { await navigator.serviceWorker.ready; - await fetch(`/__/auth/wait/${authUser?.uid}`, { method: 'HEAD' }).catch( + await fetch(`/__/auth/wait/${authUser?.uid}`, { method: "HEAD" }).catch( () => undefined, ); } @@ -67,35 +67,35 @@ export default function Header({ initialUser }) { return (
- - FriendlyEats + + FriendlyEats Friendly Eats {user ? ( <> -
+

{user.email} {user.displayName}

-
+
...
) : ( -
- - A placeholder user image + diff --git a/nextjs-end/src/components/RatingPicker.jsx b/nextjs-end/src/components/RatingPicker.jsx index f69aa72b..19183bf6 100644 --- a/nextjs-end/src/components/RatingPicker.jsx +++ b/nextjs-end/src/components/RatingPicker.jsx @@ -1,62 +1,62 @@ -import React from 'react'; +import React from "react"; // A HTML and CSS only rating picker thanks to: https://codepen.io/chris22smith/pen/MJzLJN const RatingPicker = () => { return ( -

+

-

diff --git a/nextjs-end/src/components/Restaurant.jsx b/nextjs-end/src/components/Restaurant.jsx index 11d915fa..cd34dca5 100644 --- a/nextjs-end/src/components/Restaurant.jsx +++ b/nextjs-end/src/components/Restaurant.jsx @@ -1,16 +1,16 @@ -'use client'; +"use client"; // This components shows one individual restaurant // It receives data from src/app/restaurant/[id]/page.jsx -import { React, useState, useEffect, Suspense } from 'react'; -import dynamic from 'next/dynamic'; -import { getRestaurantSnapshotById } from '@/src/lib/firebase/firestore.js'; -import { useUser } from '@/src/lib/getUser'; -import RestaurantDetails from '@/src/components/RestaurantDetails.jsx'; -import { updateRestaurantImage } from '@/src/lib/firebase/storage.js'; +import { React, useState, useEffect, Suspense } from "react"; +import dynamic from "next/dynamic"; +import { getRestaurantSnapshotById } from "@/src/lib/firebase/firestore.js"; +import { useUser } from "@/src/lib/getUser"; +import RestaurantDetails from "@/src/components/RestaurantDetails.jsx"; +import { updateRestaurantImage } from "@/src/lib/firebase/storage.js"; -const ReviewDialog = dynamic(() => import('@/src/components/ReviewDialog.jsx')); +const ReviewDialog = dynamic(() => import("@/src/components/ReviewDialog.jsx")); export default function Restaurant({ id, @@ -25,7 +25,7 @@ export default function Restaurant({ const userId = useUser()?.uid || initialUserId; const [review, setReview] = useState({ rating: 0, - text: '', + text: "", }); const onChange = (value, name) => { @@ -44,7 +44,7 @@ export default function Restaurant({ const handleClose = () => { setIsOpen(false); - setReview({ rating: 0, text: '' }); + setReview({ rating: 0, text: "" }); }; useEffect(() => { diff --git a/nextjs-end/src/components/RestaurantDetails.jsx b/nextjs-end/src/components/RestaurantDetails.jsx index 64e08e15..c3db5ef9 100644 --- a/nextjs-end/src/components/RestaurantDetails.jsx +++ b/nextjs-end/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 '@/src/components/Stars.jsx'; +import React from "react"; +import renderStars from "@/src/components/Stars.jsx"; const RestaurantDetails = ({ restaurant, @@ -12,41 +12,41 @@ const RestaurantDetails = ({ children, }) => { return ( -
+
{restaurant.name} -
+
{userId && ( review { setIsOpen(!isOpen); }} - src='/review.svg' + src="/review.svg" /> )}
-
-
+
+

{restaurant.name}

-
+
    {renderStars(restaurant.avgRating)}
({restaurant.numRatings}) @@ -55,7 +55,7 @@ const RestaurantDetails = ({

{restaurant.category} | {restaurant.city}

-

{'$'.repeat(restaurant.price)}

+

{"$".repeat(restaurant.price)}

{children}
diff --git a/nextjs-end/src/components/RestaurantListings.jsx b/nextjs-end/src/components/RestaurantListings.jsx index 270d721b..d17f1ec7 100644 --- a/nextjs-end/src/components/RestaurantListings.jsx +++ b/nextjs-end/src/components/RestaurantListings.jsx @@ -1,14 +1,14 @@ -'use client'; +"use client"; // This components handles the restaurant listings page // It receives data from src/app/page.jsx, such as the initial restaurants and search params from the URL -import Link from 'next/link'; -import { React, useState, useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import renderStars from '@/src/components/Stars.jsx'; -import { getRestaurantsSnapshot } from '@/src/lib/firebase/firestore.js'; -import Filters from '@/src/components/Filters.jsx'; +import Link from "next/link"; +import { React, useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +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 }) => (
  • @@ -26,13 +26,13 @@ const ActiveResturant = ({ restaurant }) => ( ); const ImageCover = ({ photo, name }) => ( -
    +
    {name}
    ); const ResturantDetails = ({ restaurant }) => ( -
    +

    {restaurant.name}

    @@ -40,18 +40,18 @@ const ResturantDetails = ({ restaurant }) => ( ); const RestaurantRating = ({ restaurant }) => ( -
    +
      {renderStars(restaurant.avgRating)}
    ({restaurant.numRatings})
    ); const RestaurantMetadata = ({ restaurant }) => ( -
    +

    {restaurant.category} | {restaurant.city}

    -

    {'$'.repeat(restaurant.price)}

    +

    {"$".repeat(restaurant.price)}

    ); @@ -63,10 +63,10 @@ export default function RestaurantListings({ // The initial filters are the search params from the URL, useful for when the user refreshes the page const initialFilters = { - city: searchParams.city || '', - category: searchParams.category || '', - price: searchParams.price || '', - sort: searchParams.sort || '', + city: searchParams.city || "", + category: searchParams.category || "", + price: searchParams.price || "", + sort: searchParams.sort || "", }; const [restaurants, setRestaurants] = useState(initialRestaurants); @@ -85,7 +85,7 @@ export default function RestaurantListings({ return (
    -
      +
        {restaurants.map((restaurant) => ( ))} @@ -98,7 +98,7 @@ function routerWithFilters(router, filters) { const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(filters)) { - if (value !== undefined && value !== '') { + if (value !== undefined && value !== "") { queryParams.append(key, value); } } diff --git a/nextjs-end/src/components/ReviewDialog.jsx b/nextjs-end/src/components/ReviewDialog.jsx index 6b69b63d..48cd7643 100644 --- a/nextjs-end/src/components/ReviewDialog.jsx +++ b/nextjs-end/src/components/ReviewDialog.jsx @@ -1,10 +1,10 @@ -'use client'; +"use client"; // This components handles the review dialog and uses a next.js feature known as Server Actions to handle the form submission -import { useLayoutEffect, useRef } from 'react'; -import RatingPicker from '@/src/components/RatingPicker.jsx'; -import { handleReviewFormSubmission } from '@/src/app/actions.js'; +import { useLayoutEffect, useRef } from "react"; +import RatingPicker from "@/src/components/RatingPicker.jsx"; +import { handleReviewFormSubmission } from "@/src/app/actions.js"; const ReviewDialog = ({ isOpen, @@ -48,30 +48,30 @@ const ReviewDialog = ({

        onChange(e.target.value, 'text')} + onChange={(e) => onChange(e.target.value, "text")} />

        - - + +