React keywords: BrowserRouter, createContext, lazy, Link, Navigate, NavLink, Outlet, Route, Routes, Suspense, useCallback, useContext, useEffect, useNavigate, useParams, useReducer, useSearchParams

Project overview:

Project shows: The concept of a single page application is enforced here by using jsx pages which get swapped in/out of the main App component by the users actions. Where each page file is composed of different child components. The pages are navigated to/from each other by mainupulating the url path associated with each page. The useNavigate (move to a new url) and React Router hooks are used to accomplish this.

The main App() is composed of the following nested React Router hooks:

      <BrowserRouter>
          <Suspense fallback={<SpinnerFullPage />}> – pauses the rendering of a component until its dependencies (like data) are fully ready.
            <Routes>
              <Route index element={<Homepage />} />
              <Route path=“product” element={<Product />} />
              <Route path=“pricing” element={<Pricing />} />
              <Route path=“login” element={<Login />} />
              <Route
                path=“app”
                element={
                  <ProtectedRoute>
                    <AppLayout />
                  </ProtectedRoute>
                }
              >
                <Route index element={<Navigate replace to=“cities” />} />
                <Route path=“cities” element={<CityList />} />
                <Route path=“cities/:id” element={<City />} />
                <Route path=“countries” element={<CountryList />} />
                <Route path=“form” element={<Form />} />
              </Route>
              <Route path=“*” element={<PageNotFound />} />
            </Routes>
          </Suspense>
        </BrowserRouter>

where each Route specifies a unique url path to a custom jsx page. There can be multiple <Routes>.

The pages are:

– AppLayout – contains the Sidebar(Logo and CityList/CountryList) and the Map components.

– Homepage – contains the PageNav header (Logo, PRICING/PRODUCT/LOGIN), some text and a custom button component with a Link to the Login page.

– Login – contains the PageNav header (Logo, PRICING/PRODUCT/LOGIN), a useState for the email and password, along with the form onSubmit function (called by the ‘Enter’ key press, or clicking on the <form… <Button> component) are directed to the login function of the AuthProvider, which uses a reducer to set isAuthorized to true/false. When this is set, the Login() component detects the change with useEffect, which will navigate/set the url to “/app” (actually /app/cities).

– PageNotFound – contains a text warning, and is assigned as the last <Route element… /> in the <Routes> list for the parent App() component. It is only called if the url path does not equal path=”cities’ or “cities/:id” or “countries” or “form”.

– Pricing/Product – contains the PageNav header (Logo, PRICING/PRODUCT/LOGIN) and a few paragraphs describing itself.

– ProtectedRoute – is used to redirect the url to the root “/” if the user login is not authorized. The convention is to wrap the main <AppLayout> component with the <ProtectedRoute> component.

The pages are lazy loaded, which means that each component is only loaded when it is needed, reducing the initial bundle size of the application.

There are also 17 child components that are used to build the individual pages. Almost all of the child components have a matching *.module.css file to style each component.

All of the Route hooks are wrapped by two context providers, one which provides access to cities data selected from the map, and one for the users login credentials. Both context providers use a reducer to provide access to, and manipulate city data.

When the map is clicked on the react-leaflet library fires off a useMapEvents event which we coded to add the event lat & lng coordinates to the url: “/app/form?lat=41.74144994967305&lng=-0.8349557818787503”. The Form component is called when the lat & lng are updated, which returns a <form …> to allow the user to add descriptive data about the city, after which a new city object is constructed and is added to the list of cities state variable using the CitiesContext reducer functionality.

Code:

<!DOCTYPE html>
<html lang=“en”>
  <head>
    <meta charset=“UTF-8” />
    <link rel=“icon” type=“image/png” href=“/icon.png” />
    <meta name=“viewport” content=“width=device-width, initial-scale=1.0” />
    <title>WorldWise // Keep track of your adventures</title>
  </head>
  <body>
    <div id=“root”></div>
    <script type=“module” src=“/src/main.jsx”></script>
  </body>
</html>
import { lazy, Suspense } from “react”;
import { BrowserRouter, Routes, Route, Navigate } from “react-router-dom”;

import { CitiesProvider } from “./contexts/CitiesContext”;
import { AuthProvider } from “./contexts/FakeAuthContext”;
import ProtectedRoute from “./pages/ProtectedRoute”;

import CityList from “./components/CityList”;
import CountryList from “./components/CountryList”;
import City from “./components/City”;
import Form from “./components/Form”;
import SpinnerFullPage from “./components/SpinnerFullPage”;

// import Product from “./pages/Product”;
// import Pricing from “./pages/Pricing”;
// import Homepage from “./pages/Homepage”;
// import Login from “./pages/Login”;
// import AppLayout from “./pages/AppLayout”;
// import PageNotFound from “./pages/PageNotFound”;

const Homepage = lazy(() => import(“./pages/Homepage”));
const Product = lazy(() => import(“./pages/Product”));
const Pricing = lazy(() => import(“./pages/Pricing”));
const Login = lazy(() => import(“./pages/Login”));
const AppLayout = lazy(() => import(“./pages/AppLayout”));
const PageNotFound = lazy(() => import(“./pages/PageNotFound”));

// dist/assets/index-59fcab9b.css   30.56 kB │ gzip:   5.14 kB
// dist/assets/index-f7c12d89.js   572.44 kB │ gzip: 151.29 kB

function App() {
  return (
    <AuthProvider>
      <CitiesProvider>
        <BrowserRouter>
          <Suspense fallback={<SpinnerFullPage />}>
            <Routes>
              <Route index element={<Homepage />} />
              <Route path=“product” element={<Product />} />
              <Route path=“pricing” element={<Pricing />} />
              <Route path=“login” element={<Login />} />
              <Route
                path=“app”
                element={
                  <ProtectedRoute>
                    <AppLayout />
                  </ProtectedRoute>
                }
              >
                <Route index element={<Navigate replace to=“cities” />} />
                <Route path=“cities” element={<CityList />} />
                <Route path=“cities/:id” element={<City />} />
                <Route path=“countries” element={<CountryList />} />
                <Route path=“form” element={<Form />} />
              </Route>
              <Route path=“*” element={<PageNotFound />} />
            </Routes>
          </Suspense>
        </BrowserRouter>
      </CitiesProvider>
    </AuthProvider>
  );
}

export default App;
import Map from “../components/Map”;
import Sidebar from “../components/Sidebar”;
import User from “../components/User”;

import styles from “./AppLayout.module.css”;

function AppLayout() {
  return (
    <div className={styles.app}>
      <Sidebar />
      <Map />
      <User />
    </div>
  );
}

export default AppLayout;
.app {
  height: 100vh;
  padding: 2.4rem;
  overscroll-behavior-y: none;
  display: flex;
  position: relative;
}
import { Link } from “react-router-dom”;
import PageNav from “../components/PageNav”;
import styles from “./Homepage.module.css”;

export default function Homepage() {
  return (
    <main className={styles.homepage}>
      <PageNav />

      <section>
        <h1>
          You travel the world.
          <br />
          WorldWise keeps track of your adventures.
        </h1>
        <h2>
          A world map that tracks your footsteps into every city you can think
          of. Never forget your wonderful experiences, and show your friends how
          you have wandered the world.
        </h2>
        <Link to=“/login” className=“cta”>
          Start tracking now
        </Link>
      </section>
    </main>
  );
}
.homepage {
  height: calc(100vh 5rem);
  margin: 2.5rem;
  background-image: linear-gradient(
      rgba(36, 42, 46, 0.8),
      rgba(36, 42, 46, 0.8)
    ),
    url(“/bg.jpg”);
  background-size: cover;
  background-position: center;
  padding: 2.5rem 5rem;
}

.homepage section {
  display: flex;
  flex-direction: column;
  height: 85%;
  align-items: center;
  justify-content: center;
  gap: 2.5rem;
  text-align: center;
}

.homepage h1 {
  font-size: 4.5rem;
  line-height: 1.3;
}

.homepage h2 {
  width: 90%;
  font-size: 1.9rem;
  color: var(–color-light–1);
  margin-bottom: 2.5rem;
}
import { useEffect, useState } from “react”;
import { useNavigate } from “react-router-dom”;
import Button from “../components/Button”;
import PageNav from “../components/PageNav”;
import { useAuth } from “../contexts/FakeAuthContext”;
import styles from “./Login.module.css”;

 

export default function Login() {
  // PRE-FILL FOR DEV PURPOSES
  const [email, setEmail] = useState([email protected]);
  const [password, setPassword] = useState(“qwerty”);

 

  const { login, isAuthenticated } = useAuth();
  const navigate = useNavigate();

 

  function handleSubmit(e) {
    e.preventDefault();

 

    if (email && password) login(email, password);
  }

 

  useEffect(
    function () {
      // actually navigates to /app/cities
      if (isAuthenticated) navigate(“/app”, { replace: true });
    },
    [isAuthenticated, navigate]
  );

 

  return (
    <main className={styles.login}>
      <PageNav />

 

      <form className={styles.form} onSubmit={handleSubmit}>
        <div className={styles.row}>
          <label htmlFor=“email”>Email address</label>
          <input
            type=“email”
            id=“email”
            onChange={(e) => setEmail(e.target.value)}
            value={email}
          />
        </div>

 

        <div className={styles.row}>
          <label htmlFor=“password”>Password</label>
          <input
            type=“password”
            id=“password”
            onChange={(e) => setPassword(e.target.value)}
            value={password}
          />
        </div>

 

        <div>
         {/*when the button is clicked, html automatically calls
         the onSubmit for the form that the button is part of, this
         is just how html works!*/}
          <Button type=“primary”>Login</Button>
        </div>
      </form>
    </main>
  );
}
.login {
  margin: 2.5rem;
  padding: 2.5rem 5rem;
  background-color: var(–color-dark–1);
  min-height: calc(100vh 5rem);
}

.form {
  background-color: var(–color-dark–2);
  border-radius: 7px;
  padding: 2rem 3rem;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2rem;

  /* Different from other form */
  width: 48rem;
  margin: 8rem auto;
}

.row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
export default function PageNotFound() {
  return (
    <div>
      <h1>Page not found 😢</h1>
    </div>
  );
}
// Uses the same styles as Product
import PageNav from “../components/PageNav”;
import styles from “./Product.module.css”;

export default function Product() {
  return (
    <main className={styles.product}>
      <PageNav />

      <section>
        <div>
          <h2>
            Simple pricing.
            <br />
            Just $9/month.
          </h2>
          <p>
            Lorem ipsum dolor, sit amet consectetur adipisicing elit. Vitae vel
            labore mollitia iusto. Recusandae quos provident, laboriosam fugit
            voluptatem iste.
          </p>
        </div>
        <img src=“img-2.jpg” alt=“overview of a large city with skyscrapers” />
      </section>
    </main>
  );
}
import PageNav from “../components/PageNav”;
import styles from “./Product.module.css”;

export default function Product() {
  return (
    <main className={styles.product}>
      <PageNav />

      <section>
        <img
          src=“img-1.jpg”
          alt=“person with dog overlooking mountain with sunset”
        />
        <div>
          <h2>About WorldWide.</h2>
          <p>
            Lorem ipsum dolor sit amet consectetur adipisicing elit. Illo est
            dicta illum vero culpa cum quaerat architecto sapiente eius non
            soluta, molestiae nihil laborum, placeat debitis, laboriosam at fuga
            perspiciatis?
          </p>
          <p>
            Lorem, ipsum dolor sit amet consectetur adipisicing elit. Corporis
            doloribus libero sunt expedita ratione iusto, magni, id sapiente
            sequi officiis et.
          </p>
        </div>
      </section>
    </main>
  );
}
.product {
  margin: 2.5rem;
  padding: 2.5rem 5rem;
  background-color: var(–color-dark–1);
  min-height: calc(100vh 5rem);
}

.product section {
  width: clamp(80rem, 80%, 90rem);
  margin: 6rem auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7rem;
  align-items: center;
}

.product img {
  width: 100%;
}

.product h2 {
  font-size: 4rem;
  line-height: 1.2;
  margin-bottom: 3rem;
}

.product p {
  font-size: 1.6rem;
  margin-bottom: 2rem;
}

.product section a {
  margin-top: 2rem;
}
import { useEffect } from “react”;
import { useNavigate } from “react-router-dom”;
import { useAuth } from “../contexts/FakeAuthContext”;

function ProtectedRoute({ children }) {
  const { isAuthenticated } = useAuth();
  const navigate = useNavigate();

  useEffect(
    function () {
      if (!isAuthenticated) navigate(“/”);
    },
    [isAuthenticated, navigate]
  );

  return isAuthenticated ? children : null;
}

export default ProtectedRoute;
import { useQuiz } from “../contexts/QuizContext”;

function StartScreen() {
  const { numQuestions, dispatch } = useQuiz();

  return (
    <div className=“start”>
      <h2>Welcome to The React Quiz!</h2>
      <h3>{numQuestions} questions to test your React mastery</h3>
      <button
        className=“btn btn-ui”
        onClick={() => dispatch({ type: “start” })}
      >
        Let’s start
      </button>
    </div>
  );
}

export default StartScreen;
import { NavLink } from “react-router-dom”;
import styles from “./AppNav.module.css”;

function AppNav() {
  return (
    <nav className={styles.nav}>
      <ul>
        <li>
          <NavLink to=“cities”>Cities</NavLink>
        </li>
        <li>
          <NavLink to=“countries”>Countries</NavLink>
        </li>
      </ul>
    </nav>
  );
}

export default AppNav;
.nav {
  margin-top: 3rem;
  margin-bottom: 2rem;
}

.nav ul {
  list-style: none;
  display: flex;
  background-color: var(–color-dark–2);
  border-radius: 7px;
}

.nav a:link,
.nav a:visited {
  display: block;
  color: inherit;
  text-decoration: none;
  text-transform: uppercase;
  font-size: 1.2rem;
  font-weight: 700;
  padding: 0.5rem 2rem;
  border-radius: 5px;
}

/* CSS Modules feature */
.nav a:global(.active) {
  background-color: var(–color-dark–0);
}
import { useNavigate } from “react-router-dom”;
import Button from “./Button”;

function BackButton() {
  const navigate = useNavigate();

  return (
    <Button
      type=“back”
      onClick={(e) => {
        e.preventDefault();
        navigate(1);
      }}
    >
      &larr; Back
    </Button>
  );
}

export default BackButton;
import styles from “./Button.module.css”;

function Button({ children, onClick, type }) {
  return (
    <button onClick={onClick} className={`${styles.btn} ${styles[type]}`}>
      {children}
    </button>
  );
}

export default Button;
.btn {
  color: inherit;
  text-transform: uppercase;
  padding: 0.8rem 1.6rem;
  font-family: inherit;
  font-size: 1.5rem;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.primary {
  font-weight: 700;
  background-color: var(–color-brand–2);
  color: var(–color-dark–1);
}

.back {
  font-weight: 600;
  background: none;
  border: 1px solid currentColor;
}

.position {
  font-weight: 700;
  position: absolute;
  z-index: 1000;
  font-size: 1.4rem;
  bottom: 4rem;
  left: 50%;
  transform: translateX(-50%);
  background-color: var(–color-brand–2);
  color: var(–color-dark–1);
  box-shadow: 0 0.4rem 1.2rem rgba(36, 42, 46, 0.16);
}
import { useEffect } from “react”;
import { useParams } from “react-router-dom”;
import { useCities } from “../contexts/CitiesContext”;
import BackButton from “./BackButton”;
import styles from “./City.module.css”;
import Spinner from “./Spinner”;

const formatDate = (date) =>
  new Intl.DateTimeFormat(“en”, {
    day: “numeric”,
    month: “long”,
    year: “numeric”,
    weekday: “long”,
  }).format(new Date(date));

function City() {
  const { id } = useParams();
  const { getCity, currentCity, isLoading } = useCities();

  useEffect(
    function () {
      getCity(id);
    },
    [id, getCity]
  );

  const { cityName, emoji, date, notes } = currentCity;

  if (isLoading) return <Spinner />;

  return (
    <div className={styles.city}>
      <div className={styles.row}>
        <h6>City name</h6>
        <h3>
          <span>{emoji}</span> {cityName}
        </h3>
      </div>

      <div className={styles.row}>
        <h6>You went to {cityName} on</h6>
        <p>{formatDate(date || null)}</p>
      </div>

      {notes && (
        <div className={styles.row}>
          <h6>Your notes</h6>
          <p>{notes}</p>
        </div>
      )}

      <div className={styles.row}>
        <h6>Learn more</h6>
        <a
          href={`https://en.wikipedia.org/wiki/${cityName}`}
          target=“_blank”
          rel=“noreferrer”
        >
          Check out {cityName} on Wikipedia &rarr;
        </a>
      </div>

      <div>
        <BackButton />
      </div>
    </div>
  );
}

export default City;
.city {
  padding: 2rem 3rem;
  max-height: 70%;
  background-color: var(–color-dark–2);
  border-radius: 7px;
  overflow: scroll;

  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.city h6 {
  text-transform: uppercase;
  font-size: 1.1rem;
  font-weight: 900;
  color: var(–color-light–1);
}

.city h3 {
  font-size: 1.9rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}

.city h3 span {
  font-size: 3.2rem;
  line-height: 1;
}

.city p {
  font-size: 1.6rem;
}

.city a:link,
.city a:visited {
  font-size: 1.6rem;
  color: var(–color-brand–1);
}
import { Link } from “react-router-dom”;
import { useCities } from “../contexts/CitiesContext”;
import styles from “./CityItem.module.css”;

 

const formatDate = (date) =>
  new Intl.DateTimeFormat(“en”, {
    day: “numeric”,
    month: “long”,
    year: “numeric”,
  }).format(new Date(date));

 

function CityItem({ city }) {
  const { currentCity, deleteCity } = useCities();
  const { cityName, emoji, date, id, position } = city;

 

  function handleClick(e) {
    e.preventDefault();
    deleteCity(id);
  }

 

  return (
    <li>
      <Link
        className={`${styles.cityItem} ${
          id === currentCity.id ? styles[“cityItem–active”] : “”
        }`}
        to={`${id}?lat=${position.lat}&lng=${position.lng}`}
      >
        <span className={styles.emoji}>{emoji}</span>
        <h3 className={styles.name}>{cityName}</h3>
        <time className={styles.date}>({formatDate(date)})</time>
        <button className={styles.deleteBtn} onClick={handleClick}>
          &times;
        </button>
      </Link>
    </li>
  );
}

 

export default CityItem;
.cityItem,
.cityItem:link,
.cityItem:visited {
  display: flex;
  gap: 1.6rem;
  align-items: center;

  background-color: var(–color-dark–2);
  border-radius: 7px;
  padding: 1rem 2rem;
  border-left: 5px solid var(–color-brand–2);
  cursor: pointer;

  color: inherit;
  text-decoration: none;
}

.cityItem–active {
  border: 2px solid var(–color-brand–2);
  border-left: 5px solid var(–color-brand–2);
}

.emoji {
  font-size: 2.6rem;
  line-height: 1;
}

.name {
  font-size: 1.7rem;
  font-weight: 600;
  margin-right: auto;
}

.date {
  font-size: 1.5rem;
}

.deleteBtn {
  height: 2rem;
  aspect-ratio: 1;
  border-radius: 50%;
  border: none;
  background-color: var(–color-dark–1);
  color: var(–color-light–2);
  font-size: 1.6rem;
  font-weight: 400;
  cursor: pointer;
  transition: all 0.2s;
}

.deleteBtn:hover {
  background-color: var(–color-brand–1);
  color: var(–color-dark–1);
}
import Spinner from “./Spinner”;
import styles from “./CityList.module.css”;
import CityItem from “./CityItem”;
import Message from “./Message”;
import { useCities } from “../contexts/CitiesContext”;

 

function CityList() {
  const { cities, isLoading } = useCities();

 

  if (isLoading) return <Spinner />;

 

  if (!cities.length)
    return (
      <Message message=“Add your first city by clicking on a city on the map” />
    );

 

  return (
    <ul className={styles.cityList}>
      {cities.map((city) => (
        <CityItem city={city} key={city.id} />
      ))}
    </ul>
  );
}

 

export default CityList;
.cityList {
  width: 100%;
  height: 65vh;
  list-style: none;
  overflow-y: scroll;
  overflow-x: hidden;

  display: flex;
  flex-direction: column;
  gap: 1.4rem;
}

.cityList::-webkit-scrollbar {
  width: 0;
}
import styles from “./CountryItem.module.css”;

 

function CountryItem({ country }) {
  return (
    <li className={styles.countryItem}>
      <span>{country.emoji}</span>
      <span>{country.country}</span>
    </li>
  );
}

 

export default CountryItem;
.countryItem {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.2rem;

 

  font-size: 1.7rem;
  font-weight: 600;

 

  background-color: var(–color-dark–2);
  border-radius: 7px;
  padding: 1rem 2rem;
  border-left: 5px solid var(–color-brand–1);
}

 

.countryItem span:first-child {
  font-size: 3rem;
  line-height: 1;
}
import Spinner from “./Spinner”;
import styles from “./CountryList.module.css”;
import CountryItem from “./CountryItem”;
import Message from “./Message”;
import { useCities } from “../contexts/CitiesContext”;

function CountryList() {
  const { cities, isLoading } = useCities();

  if (isLoading) return <Spinner />;

  if (!cities.length)
    return (
      <Message message=“Add your first city by clicking on a city on the map” />
    );

  const countries = cities.reduce((arr, city) => {
    if (!arr.map((el) => el.country).includes(city.country))
      return [arr, { country: city.country, emoji: city.emoji }];
    else return arr;
  }, []);

  return (
    <ul className={styles.countryList}>
      {countries.map((country) => (
        <CountryItem country={country} key={country.country} />
      ))}
    </ul>
  );
}

export default CountryList;
.countryList {
  width: 100%;
  height: 65vh;
  list-style: none;
  overflow-y: scroll;
  overflow-x: hidden;

  display: grid;
  grid-template-columns: 1fr 1fr;
  align-content: start;
  gap: 1.6rem;
}
// “https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=0&longitude=0”

 

import { useEffect, useState } from “react”;
import DatePicker from “react-datepicker”;
import “react-datepicker/dist/react-datepicker.css”;

 

import Button from “./Button”;
import BackButton from “./BackButton”;

 

import styles from “./Form.module.css”;
import { useUrlPosition } from “../hooks/useUrlPosition”;
import Message from “./Message”;
import Spinner from “./Spinner”;
import { useCities } from “../contexts/CitiesContext”;
import { useNavigate } from “react-router-dom”;

 

export function convertToEmoji(countryCode) {
  const codePoints = countryCode
    .toUpperCase()
    .split(“”)
    .map((char) => 127397 + char.charCodeAt());
  return String.fromCodePoint(codePoints);
}

 

const BASE_URL = “https://api.bigdatacloud.net/data/reverse-geocode-client”;

 

function Form() {
  const [lat, lng] = useUrlPosition();
  const { createCity, isLoading } = useCities();
  const navigate = useNavigate();

 

  const [isLoadingGeocoding, setIsLoadingGeocoding] = useState(false);
  const [cityName, setCityName] = useState(“”);
  const [country, setCountry] = useState(“”);
  const [date, setDate] = useState(new Date());
  const [notes, setNotes] = useState(“”);
  const [emoji, setEmoji] = useState(“”);
  const [geocodingError, setGeocodingError] = useState(“”);
  const [id, setId] = useState(“”);

 

  useEffect(
    function () {
      if (!lat && !lng) return;

 

      async function fetchCityData() {
        try {
          setIsLoadingGeocoding(true);
          setGeocodingError(“”);

 

          const res = await fetch(
            `${BASE_URL}?latitude=${lat}&longitude=${lng}`
          );
          const data = await res.json();
          console.log(data);
          const cityObject = data.localityInfo.administrative.find(
            (o) => o.name === data.city
          );
          console.log(cityObject.geonameId);

 

          if (!data.countryCode)
            throw new Error(
              “That doesn’t seem to be a city. Click somewhere else 😉”
            );

 

          setCityName(data.city || data.locality || “”);
          setCountry(data.countryName);
          setEmoji(convertToEmoji(data.countryCode));
          setId(cityObject.geonameId);
        } catch (err) {
          setGeocodingError(err.message);
        } finally {
          setIsLoadingGeocoding(false);
        }
      }
      fetchCityData();
    },
    [lat, lng]
  );

 

  async function handleSubmit(e) {
    e.preventDefault();

 

    if (!cityName || !date) return;

 

    const newCity = {
      cityName,
      country,
      emoji,
      date,
      notes,
      position: { lat, lng },
      id,
    };

 

    await createCity(newCity);
    navigate(“/app/cities”);
  }

 

  if (isLoadingGeocoding) return <Spinner />;

 

  if (!lat && !lng)
    return <Message message=“Start by clicking somewhere on the map” />;

 

  if (geocodingError) return <Message message={geocodingError} />;

 

  return (
    <form
      className={`${styles.form} ${isLoading ? styles.loading : “”}`}
      onSubmit={handleSubmit}
    >
      <div className={styles.row}>
        <label htmlFor=“cityName”>City name</label>
        <input
          id=“cityName”
          onChange={(e) => setCityName(e.target.value)}
          value={cityName}
        />
        <span className={styles.flag}>{emoji}</span>
      </div>

 

      <div className={styles.row}>
        <label htmlFor=“date”>When did you go to {cityName}?</label>

 

        <DatePicker
          id=“date”
          onChange={(date) => setDate(date)}
          selected={date}
          dateFormat=“dd/MM/yyyy”
        />
      </div>

 

      <div className={styles.row}>
        <label htmlFor=“notes”>Notes about your trip to {cityName}</label>
        <textarea
          id=“notes”
          onChange={(e) => setNotes(e.target.value)}
          value={notes}
        />
      </div>

 

      <div className={styles.buttons}>
        {/*when the button is clicked, html automatically calls
         the onSubmit for the form that the button is part of, this
         is just how html works!*/}
        <Button type=“primary”>Add</Button>
        <BackButton />
      </div>
    </form>
  );
}

 

export default Form;
.form {
  background-color: var(–color-dark–2);
  border-radius: 7px;
  padding: 2rem 3rem;
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2rem;
}

.row {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  position: relative;
}

.buttons {
  display: flex;
  justify-content: space-between;
}

.flag {
  position: absolute;
  right: 1rem;
  top: 2.7rem;
  font-size: 2.8rem;
}

.form.loading {
  opacity: 0.3;
}

.form.loading button {
  pointer-events: none;
  background-color: var(–color-light–1);
  border: 1px solid var(–color-light–1);
  color: var(–color-dark–0);
}

:global(.react-datepicker) {
  font-family: inherit;
  font-size: 1.2rem;
}
import { Link } from “react-router-dom”;
import styles from “./Logo.module.css”;

function Logo() {
  return (
    <Link to=“/”>
      <img src=“/logo.png” alt=“WorldWise logo” className={styles.logo} />
    </Link>
  );
}

export default Logo;
.logo {
  height: 5.2rem;
}
import { useNavigate } from “react-router-dom”;
import {
  MapContainer,
  TileLayer,
  Marker,
  Popup,
  useMap,
  useMapEvents,
} from “react-leaflet”;

import styles from “./Map.module.css”;
import { useEffect, useState } from “react”;
import { useCities } from “../contexts/CitiesContext”;
import { useGeolocation } from “../hooks/useGeolocation”;
import { useUrlPosition } from “../hooks/useUrlPosition”;
import Button from “./Button”;

function Map() {
  const { cities } = useCities();
  const [mapPosition, setMapPosition] = useState([40, 0]);
  const {
    isLoading: isLoadingPosition,
    position: geolocationPosition,
    getPosition,
  } = useGeolocation();
  const [mapLat, mapLng] = useUrlPosition();

  useEffect(
    function () {
      if (mapLat && mapLng) setMapPosition([mapLat, mapLng]);
    },
    [mapLat, mapLng]
  );

  useEffect(
    function () {
      if (geolocationPosition)
        setMapPosition([geolocationPosition.lat, geolocationPosition.lng]);
    },
    [geolocationPosition]
  );

  return (
    <div className={styles.mapContainer}>
      {!geolocationPosition && (
        <Button type=“position” onClick={getPosition}>
          {isLoadingPosition ? “Loading…” : “Use your position”}
        </Button>
      )}

      <MapContainer
        center={mapPosition}
        zoom={6}
        scrollWheelZoom={true}
        className={styles.map}
      >
        <TileLayer
          attribution=&copy; <a href=”https://www.openstreetmap.org/copyright”>OpenStreetMap</a> contributors’
          url=“https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png”
        />
        {cities.map((city) => (
          <Marker
            position={[city.position.lat, city.position.lng]}
            key={city.id}
          >
            <Popup>
              <span>{city.emoji}</span> <span>{city.cityName}</span>
            </Popup>
          </Marker>
        ))}

        <ChangeCenter position={mapPosition} />
        <DetectClick />
      </MapContainer>
    </div>
  );
}

function ChangeCenter({ position }) {
  const map = useMap();
  map.setView(position);
  return null;
}

function DetectClick() {
  const navigate = useNavigate();

  useMapEvents({
    click: (e) => navigate(`form?lat=${e.latlng.lat}&lng=${e.latlng.lng}`),
  });
}
export default Map;
.mapContainer {
  flex: 1;
  height: 100%;
  background-color: var(–color-dark–2);
  position: relative;
}

.map {
  height: 100%;
}

/* Here we want to style classes that are coming from leaflet. So we want CSS Modules to give us the ACTUAL classnames, not to add some random ID to them, because then they won’t match the classnames defined inside the map. The solution is to define these classes as GLOBAL */
:global(.leaflet-popup .leaflet-popup-content-wrapper) {
  background-color: var(–color-dark–1);
  color: var(–color-light–2);
  border-radius: 5px;
  padding-right: 0.6rem;
}

:global(.leaflet-popup .leaflet-popup-content) {
  font-size: 1.5rem;
  display: flex;
  align-items: center;
  gap: 1rem;
}

:global(.leaflet-popup .leaflet-popup-content span:first-child) {
  font-size: 2.5rem;
  line-height: 1;
}

:global(.leaflet-popup .leaflet-popup-tip) {
  background-color: var(–color-dark–1);
}

:global(.leaflet-popup-content-wrapper) {
  border-left: 5px solid var(–color-brand–2);
}
import styles from “./Message.module.css”;

function Message({ message }) {
  return (
    <p className={styles.message}>
      <span role=“img”>👋</span> {message}
    </p>
  );
}

export default Message;
.message {
  text-align: center;
  font-size: 1.8rem;
  width: 80%;
  margin: 2rem auto;
  font-weight: 600;
}
import Logo from “./Logo”;
import styles from “./PageNav.module.css”;

function PageNav() {
  return (
    <nav className={styles.nav}>
      <Logo />

      <ul>
        <li>
          <NavLink to=“/pricing”>Pricing</NavLink>
        </li>
        <li>
          <NavLink to=“/product”>Product</NavLink>
        </li>
        <li>
          <NavLink to=“/login” className={styles.ctaLink}>
            Login
          </NavLink>
        </li>
      </ul>
    </nav>
  );
}

export default PageNav;
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.nav ul {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 4rem;
}

.nav a:link,
.nav a:visited {
  text-decoration: none;
  color: var(–color-light–2);
  text-transform: uppercase;
  font-size: 1.5rem;
  font-weight: 600;
}

/* CSS Modules feature */
.nav a:global(.active) {
  color: var(–color-brand–2);
}

a.ctaLink:link,
a.ctaLink:visited {
  background-color: var(–color-brand–2);
  color: var(–color-dark–0);
  padding: 0.8rem 2rem;
  border-radius: 7px;
}
import { Outlet } from “react-router-dom”;
import AppNav from “./AppNav”;
import Logo from “./Logo”;
import styles from “./Sidebar.module.css”;

function Sidebar() {
  return (
    <div className={styles.sidebar}>
      <Logo />
      <AppNav />

      <Outlet />

      <footer className={styles.footer}>
        <p className={styles.copyright}>
          &copy; Copyright {new Date().getFullYear()} by WorldWise Inc.
        </p>
      </footer>
    </div>
  );
}

export default Sidebar;
.sidebar {
  flex-basis: 56rem;
  background-color: var(–color-dark–1);
  padding: 3rem 5rem 3.5rem 5rem;

  display: flex;
  flex-direction: column;
  align-items: center;
  height: calc(100vh 4.8rem);
}

.footer {
  margin-top: auto;
}

.copyright {
  font-size: 1.2rem;
  color: var(–color-light–1);
}
import styles from “./Spinner.module.css”;

function Spinner() {
  return (
    <div className={styles.spinnerContainer}>
      <div className={styles.spinner}></div>
    </div>
  );
}

export default Spinner;
.spinnerContainer {
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.spinner {
  width: 6rem;
  height: 6rem;
  border-radius: 50%;
  background: conic-gradient(#0000 10%, var(–color-light–2));
  -webkit-mask: radial-gradient(farthest-side, #0000 calc(100% 8px), #000 0);
  animation: rotate 1.5s infinite linear;
}

@keyframes rotate {
  to {
    transform: rotate(1turn);
  }
}
import Spinner from “./Spinner”;
import styles from “./SpinnerFullPage.module.css”;

function SpinnerFullPage() {
  return (
    <div className={styles.spinnerFullpage}>
      <Spinner />
    </div>
  );
}

export default SpinnerFullPage;
.spinnerFullpage {
  margin: 2.5rem;
  height: calc(100vh 5rem);
  background-color: var(–color-dark–1);
}
import { useNavigate } from “react-router-dom”;
import { useAuth } from “../contexts/FakeAuthContext”;
import styles from “./User.module.css”;

function User() {
  const { user, logout } = useAuth();
  const navigate = useNavigate();

  function handleClick() {
    logout();
    navigate(“/”);
  }

  return (
    <div className={styles.user}>
      <img src={user.avatar} alt={user.name} />
      <span>Welcome, {user.name}</span>
      <button onClick={handleClick}>Logout</button>
    </div>
  );
}

export default User;

/*
CHALLENGE

1) Add `AuthProvider` to `App.jsx`
2) In the `Login.jsx` page, call `login()` from context
3) Inside an effect, check whether `isAuthenticated === true`. If so, programatically navigate to `/app`
4) In `User.js`, read and display logged in user from context (`user` object). Then include this component in `AppLayout.js`
5) Handle logout button by calling `logout()` and navigating back to `/`
*/
.user {
  position: absolute;
  top: 4.2rem;
  right: 4.2rem;
  background-color: var(–color-dark–1);
  padding: 1rem 1.4rem;
  border-radius: 7px;
  z-index: 999;
  box-shadow: 0 0.8rem 2.4rem rgba(36, 42, 46, 0.5);
  font-size: 1.6rem;
  font-weight: 600;

  display: flex;
  align-items: center;
  gap: 1.6rem;
}

.user img {
  border-radius: 100px;
  height: 4rem;
}

.user button {
  background-color: var(–color-dark–2);
  border-radius: 7px;
  border: none;
  padding: 0.6rem 1.2rem;
  color: inherit;
  font-family: inherit;
  font-size: 1.2rem;
  font-weight: 700;
  text-transform: uppercase;
  cursor: pointer;
}
import {
  createContext,
  useEffect,
  useContext,
  useReducer,
  useCallback,
} from “react”;

//const BASE_URL = “http://localhost:9000”;

const CitiesContext = createContext();

const initialState = {
  cities: [],
  isLoading: false,
  currentCity: {},
  error: “”,
};

const initialCities = [
  {
    cityName: “Lisbon”,
    country: “Portugal”,
    emoji: “🇵🇹”,
    date: “2027-10-31T15:59:59.138Z”,
    notes: “My favorite city so far!”,
    position: {
      lat: 38.727881642324164,
      lng: 9.140900099907554,
    },
    id: 73930385,
  },
  {
    cityName: “Madrid”,
    country: “Spain”,
    emoji: “🇪🇸”,
    date: “2027-07-15T08:22:53.976Z”,
    notes: “”,
    position: {
      lat: 40.46635901755316,
      lng: 3.7133789062500004,
    },
    id: 17806751,
  },
  {
    cityName: “Berlin”,
    country: “Germany”,
    emoji: “🇩🇪”,
    date: “2027-02-12T09:24:11.863Z”,
    notes: “Amazing 😃”,
    position: {
      lat: 52.53586782505711,
      lng: 13.376933665713324,
    },
    id: 98443197,
  },
  {
    cityName: “Nijar”,
    country: “Spain”,
    emoji: “🇪🇸”,
    date: “2023-04-03T07:47:59.202Z”,
    notes: “”,
    position: {
      lat: “36.967508314568164”,
      lng: “-2.13128394200588”,
    },
    id: 98443198,
  },
  {
    cityName: “Edmonton”,
    country: “Canada”,
    emoji: “🇨🇦”,
    date: “2024-11-09T23:41:58.374Z”,
    notes: “Not exactly Cowtown…”,
    position: {
      lat: “53.50024194411396”,
      lng: “-113.49321941048235”,
    },
    id: 98443199,
  },
];

function reducer(state, action) {
  switch (action.type) {
    case “loading”:
      return { state, isLoading: true };

    case “cities/loaded”:
      return {
        state,
        isLoading: false,
        cities: action.payload,
      };

    case “city/loaded”:
      return { state, isLoading: false, currentCity: action.payload };

    case “city/created”:
      return {
        state,
        isLoading: false,
        cities: [state.cities, action.payload],
        currentCity: action.payload,
      };

    case “city/deleted”:
      return {
        state,
        isLoading: false,
        cities: state.cities.filter((city) => city.id !== action.payload),
        currentCity: {},
      };

    case “rejected”:
      return {
        state,
        isLoading: false,
        error: action.payload,
      };

    default:
      throw new Error(“Unknown action type”);
  }
}

function CitiesProvider({ children }) {
  const [{ cities, isLoading, currentCity, error }, dispatch] = useReducer(
    reducer,
    initialState
  );

  useEffect(function () {
    async function fetchCities() {
      dispatch({ type: “loading” });

      try {
        //const res = await fetch(`${BASE_URL}/cities`);
        //const data = await res.json();
        dispatch({ type: “cities/loaded”, payload: initialCities });
      } catch {
        dispatch({
          type: “rejected”,
          payload: “There was an error loading cities…”,
        });
      }
    }
    fetchCities();
  }, []);

  const getCity = useCallback(
    /*async function getCity(id) {*/
    function getCity(id) {
      if (Number(id) === currentCity.id) return;

      dispatch({ type: “loading” });

      try {
        /*const res = await fetch(`${BASE_URL}/cities/${id}`);
        const data = await res.json();*/
        const data = cities.find((city) => city.id === Number(id));
        console.log(id);
        dispatch({ type: “city/loaded”, payload: data });
      } catch {
        dispatch({
          type: “rejected”,
          payload: “There was an error loading the city…”,
        });
      }
    },
    [currentCity.id, cities]
  );

  /*async function createCity(newCity) {*/
  function createCity(newCity) {
    dispatch({ type: “loading” });

    try {
      /*const res = await fetch(`${BASE_URL}/cities`, {
        method: “POST”,
        body: JSON.stringify(newCity),
        headers: {
          “Content-Type”: “application/json”,
        },
      });
      const data = await res.json();*/
      console.log(newCity);
      dispatch({ type: “city/created”, payload: newCity });
    } catch {
      dispatch({
        type: “rejected”,
        payload: “There was an error creating the city…”,
      });
    }
  }

  /*async function deleteCity(id) {*/
  function deleteCity(id) {
    dispatch({ type: “loading” });

    try {
      /*await fetch(`${BASE_URL}/cities/${id}`, {
        method: “DELETE”,
      });*/

      dispatch({ type: “city/deleted”, payload: id });
    } catch {
      dispatch({
        type: “rejected”,
        payload: “There was an error deleting the city…”,
      });
    }
  }

  return (
    <CitiesContext.Provider
      value={{
        cities,
        isLoading,
        currentCity,
        error,
        getCity,
        createCity,
        deleteCity,
      }}
    >
      {children}
    </CitiesContext.Provider>
  );
}

function useCities() {
  const context = useContext(CitiesContext);
  if (context === undefined)
    throw new Error(“CitiesContext was used outside the CitiesProvider”);
  return context;
}

export { CitiesProvider, useCities };
import { createContext, useContext, useReducer } from “react”;

const AuthContext = createContext();

const initialState = {
  user: null,
  isAuthenticated: false,
};

function reducer(state, action) {
  switch (action.type) {
    case “login”:
      return { state, user: action.payload, isAuthenticated: true };
    case “logout”:
      return { state, user: null, isAuthenticated: false };
    default:
      throw new Error(“Unknown action”);
  }
}

const FAKE_USER = {
  name: “Jack”,
  email: [email protected],
  password: “qwerty”,
  avatar: “https://i.pravatar.cc/100?u=zz”,
};

function AuthProvider({ children }) {
  const [{ user, isAuthenticated }, dispatch] = useReducer(
    reducer,
    initialState
  );

  function login(email, password) {
    if (email === FAKE_USER.email && password === FAKE_USER.password)
      dispatch({ type: “login”, payload: FAKE_USER });
  }

  function logout() {
    dispatch({ type: “logout” });
  }

  return (
    <AuthContext.Provider value={{ user, isAuthenticated, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

function useAuth() {
  const context = useContext(AuthContext);
  if (context === undefined)
    throw new Error(“AuthContext was used outside AuthProvider”);
  return context;
}

export { AuthProvider, useAuth };
import { useState } from “react”;

export function useGeolocation(defaultPosition = null) {
  const [isLoading, setIsLoading] = useState(false);
  const [position, setPosition] = useState(defaultPosition);
  const [error, setError] = useState(null);

  function getPosition() {
    if (!navigator.geolocation)
      return setError(“Your browser does not support geolocation”);

    setIsLoading(true);
    navigator.geolocation.getCurrentPosition(
      (pos) => {
        setPosition({
          lat: pos.coords.latitude,
          lng: pos.coords.longitude,
        });
        setIsLoading(false);
      },
      (error) => {
        setError(error.message);
        setIsLoading(false);
      }
    );
  }

  return { isLoading, position, error, getPosition };
}
import { useSearchParams } from “react-router-dom”;

export function useUrlPosition() {
  const [searchParams] = useSearchParams();
  const lat = searchParams.get(“lat”);
  const lng = searchParams.get(“lng”);

  return [lat, lng];
}
/* Taken from getting started guide at: https://leafletjs.com/examples/quick-start/ */
@import “https://unpkg.com/[email protected]/dist/leaflet.css”;
@import “https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&display=swap”;

/* These CSS variables are global, so they are available in all CSS modules */
:root {
  –color-brand–1: #ffb545;
  –color-brand–2: #00c46a;

  –color-dark–0: #242a2e;
  –color-dark–1: #2d3439;
  –color-dark–2: #42484d;
  –color-light–1: #aaa;
  –color-light–2: #ececec;
  –color-light–3: #d6dee0;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: inherit;
}

html {
  font-size: 62.5%;
  box-sizing: border-box;
}

body {
  font-family: “Manrope”, sans-serif;
  color: var(–color-light–2);
  font-weight: 400;
  line-height: 1.6;
}

label {
  font-size: 1.6rem;
  font-weight: 600;
}

input,
textarea {
  width: 100%;
  padding: 0.8rem 1.2rem;
  font-family: inherit;
  font-size: 1.6rem;
  border: none;
  border-radius: 5px;
  background-color: var(–color-light–3);
  transition: all 0.2s;
}

input:focus {
  outline: none;
  background-color: #fff;
}

.cta:link,
.cta:visited {
  display: inline-block;
  background-color: var(–color-brand–2);
  color: var(–color-dark–1);
  text-transform: uppercase;
  text-decoration: none;
  font-size: 1.6rem;
  font-weight: 600;
  padding: 1rem 3rem;
  border-radius: 5px;
}

/*
“importCSSModule”: {
    “prefix”: “csm”,
    “scope”: “javascript,typescript,javascriptreact”,
    “body”: [“import styles from ‘./${TM_FILENAME_BASE}.module.css'”],
    “description”: “Import CSS Module as `styles`”
  },
*/