Skip to content

john-org/6-express-react

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

10 Commits
 
 
 
 
 
 

Repository files navigation

Express and React

Assignment

Customize the backend and client work with your own content to create a full stack website. Edit the schema to conform to you content. Edit the client to display any content that you add.

  • CRUD operations are optional
  • CSS and design are optional

Reading

You should have read and stepped through the useState and useEffect documentation. Use Code Sandbox to examine the code samples.

Read:

Exercise: React Front End

Clone this repo into your projects folder and cd into it.

Remove the .git directory with rm -rf .git.

Backend

  • cd into the backend,
  • initialize it as a git repo,
  • NPM install all dependencies,
  • start Docker and run docker run --name recipes-mongo -dit -p 27017:27017 --rm mongo:4.4.1
  • start the backend: npm run dev
  • test by visiting localhost on port 3456.

Note the scripts in package.json:

"scripts": {
  "start": "NODE_ENV=production node server.js",
  "dev": "NODE_ENV=development nodemon server.js"
},

Note the following in server.js:

if (process.env.NODE_ENV === "production") {
  require("dotenv").config();
}

Environment Variables

Go to MongoDb and sign in to your account. Find the Cluster you created and click on 'Connect' to get the connection string.

Replace the database variable in backend/.env.sample with your own and rename the file to .env. Review the Database Access and Network Access settings on MongoDb. Check to ensure that your IP address settings are current or set to 'all.'

Ensure that your local DB is running:

docker run --name recipes-mongo -dit -p 27017:27017 --rm mongo:4.4.1

npm install and run npm run dev to test. You should see the vanilla js site we constructed. Be sure to test the /api/recipes endpoint as well.

Note: install a JSON formatter for your browser.

Chrome or use Firefox Developer Edition.

Create a React Project

cd into the top level of the project directory and run Create React App:

npx create-react-app client

CD into the client folder and remove the contents of the src folder and create an initial index.js file:

$ cd client
$ rm src/*
$ touch src/index.js

in client/.vscode/settings.json:

{
  "workbench.colorCustomizations": {
    "titleBar.activeForeground": "#fff",
    "titleBar.inactiveForeground": "#ffffffcc",
    "titleBar.activeBackground": "#235694",
    "titleBar.inactiveBackground": "#235694CC"
  }
}

Create a simple start page in index.js:

import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

const container = document.getElementById("root");
const root = createRoot(container);

root.render(<App />);

Set a Proxy in the React client package.json.

To use the Heroku app:

"proxy": "https://recipes-do-not-delete.herokuapp.com/",

To use your local db (make sure you start the backend using npm run dev)

"proxy": "http://localhost:3456/",

This will enable us to use 'fetch(/api/recipes)' instead of 'fetch(http://localhost:3456/api/recipes)'.

Create App.js:

import React from "react";

function App() {
  const [recipes, setRecipes] = React.useState([]);

  React.useEffect(() => {
    fetch(`/api/recipes`)
      .then((response) => response.json())
      .then((data) => console.log(data));
  });

  return (
    <div>
      <p>Hello from App</p>
    </div>
  );
}

export default App;

Add some basic CSS:

import React from "react";
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";

const container = document.getElementById("root");
const root = createRoot(container);

root.render(<App />);

Add a new index.css file in src:

body {
  max-width: 940px;
  margin: 0 auto;
  font-family: sans-serif;
  color: #333;
  background-color: #eee;
}

a {
  color: #007eb6;
}

main {
  padding: 1rem;
  background-color: #fff;
}

summary {
  margin: 1rem 0;
  border-bottom: 1px dotted #666;
}

img {
  max-width: 200px;
}

input,
textarea {
  font-size: 1rem;
  display: block;
  margin: 1rem;
  width: 90%;
  padding: 0.5rem;
  font-family: inherit;
}

label {
  margin: 1rem;
  padding: 0.5rem;
}

button {
  color: #fff;
  font-size: 1rem;
  padding: 0.5rem;
  margin: 0 1rem;
  background: #007eb6;
  border: none;
  border-radius: 3px;
}

button.delete {
  background: unset;
  margin: unset;
  border: none;
  padding: 0;
  color: #007eb6;
  cursor: pointer;
}

Note: CORS

If you ever need to deal with CORS, add the following middleware to server.js:

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
  );
  res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE");
  next();
});

Displaying Data

Use a Recipe component to display the recipes.

In App.js:

import React from "react";

function App() {
  const [recipes, setRecipes] = React.useState([]);

  React.useEffect(() => {
    fetch(`/api/recipes`)
      .then((response) => response.json())
      .then((data) => setRecipes(data));
  }, []);

  return (
    <div>
      {recipes.map((recipe) => (
        <Recipe key={recipe._id} recipe={recipe} />
      ))}
    </div>
  );
}

function Recipe(props) {
  return <p>{props.recipe.title}</p>;
}

export default App;

Demo - the problem with not adding an empty dependency array:

React.useEffect(() => {
  console.log(" useEffect running ");
  fetch(`/api/recipes`)
    .then((response) => response.json())
    .then((data) => setRecipes(data));
});

Multiple Components

Breakout the Recipe component into a separate src/Recipe.js file:

import React from "react";

function Recipe(props) {
  return <p>{props.recipe.title}</p>;
}

export default Recipe;

Import it and compose it in App.js and test.

Scaffold the Recipe component:

import React from "react";

function Recipe({ recipe }) {
  const { title, year, description, image, _id } = recipe;
  return (
    <summary>
      <img src={`img/${image}`} alt={title} />
      <h3>
        <a href={_id}>{title}</a>
      </h3>
      <p>{description}</p>
      <small>Published: {year}</small>
    </summary>
  );
}

export default Recipe;

In preparation for the next steps, create two new components. We'll use these components in the next steps to explore routing.

A Recipes component:

import React from "react";
import Recipe from "./Recipe";

function Recipes({ recipes }) {
  return (
    <summary>
      {recipes.map((recipe) => (
        <Recipe key={recipe._id} recipe={recipe} />
      ))}
    </summary>
  );
}

export default Recipes;

And a RecipeDetail component:

import React from "react";

function RecipeDetail(props) {
  return (
    <details>
      <pre>{JSON.stringify(props, null, 2)}</pre>
    </details>
  );
}

export default RecipeDetail;

App.js imports and renders Recipes.js:

import React from "react";
import Recipes from "./Recipes";

function App() {
  const [recipes, setRecipes] = React.useState([]);

  React.useEffect(() => {
    fetch(`/api/recipes`)
      .then((response) => response.json())
      .then((data) => setRecipes(data));
  }, []);

  return (
    <main>
      <Recipes recipes={recipes} />
    </main>
  );
}

export default App;

Demo - in Recipe.js:

{ process.env.NODE_ENV === "production" ? "prod" : "dev" }
<small>
  You are running this application in <b>{process.env.NODE_ENV}</b> mode.
</small>;

Client Side Routing

Up until this point, you have dealt with simple projects that do not require transitioning from one view to another. You have yet to work with Routing in React.

In SPAs, routing is the ability to move between different parts of an application when a user enters a URL or clicks an element without actually going to a new HTML document or refreshing the page.

We could build a "list / detail" type site without routing but it is important to have an introduction to it so that you can use it in larger projects.

To begin exploring client side routing we'll use the React Router.

Note: be sure you are cd'd into the client directory before installing React related packages.

npm install the latest version of react router and import the router into App.

npm i react-router-dom

Begin configuring App.js for routing:

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Recipes from "./Recipes";
import RecipeDetail from "./RecipeDetail";

function App() {
  const [recipes, setRecipes] = React.useState([]);

  React.useEffect(() => {
    fetch(`/api/recipes`)
      .then((response) => response.json())
      .then((data) => setRecipes(data));
  }, []);

  return (
    <main>
      <BrowserRouter>
        <Recipes recipes={recipes} />
      </BrowserRouter>
    </main>
  );
}

export default App;

Add a Route:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Recipes recipes={recipes} />} />
  </Routes>
</BrowserRouter>

Add a second Route:

<BrowserRouter>
  <Routes>
    <Route path="/" element={<Recipes recipes={recipes} />} />
    <Route path="/:recipeId" element={<RecipeDetail recipes={recipes} />} />
  </Routes>
</BrowserRouter>

Use the router's Link component in Recipe.js:

import React from "react";
import { Link } from "react-router-dom";

function Recipe({ recipe }) {
  const { title, year, description, image, _id } = recipe;
  return (
    <summary>
      <img src={`img/${image}`} alt={title} />
      <h3>
        <Link to={_id}>{title}</Link>
      </h3>
      <p>{description}</p>
      <small>Published: {year}</small>
    </summary>
  );
}

export default Recipe;

Check for browser refresh on the new route by watching the console.

Demo - note that the component renders twice. Try using an object instead of an array to initialize state:

const [recipes, setRecipes] = React.useState({});

Here is the entire App component:

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Recipes from "./Recipes";
import RecipeDetail from "./RecipeDetail";

function App() {
  const [recipes, setRecipes] = React.useState([]);

  React.useEffect(() => {
    fetch(`/api/recipes`)
      .then((response) => response.json())
      .then((data) => setRecipes(data));
  }, []);

  return (
    <main>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<Recipes recipes={recipes} />} />
          <Route
            path="/:recipeId"
            element={<RecipeDetail recipes={recipes} />}
          />
        </Routes>
      </BrowserRouter>
    </main>
  );
}

export default App;

App.js imports and renders Recipes.js and renders either Recipes.js or RecipesDetail.js depending on the Route.

Edit the RecipeDetail component:

import React from "react";
import { useParams } from "react-router-dom";

function RecipeDetail(props) {
  const { recipeId } = useParams();
  const currRecipe = props.recipes.filter((recipe) => recipe._id === recipeId);
  console.log("currRecipe[0]", currRecipe[0]);
  // const thisRecipe = { ...currRecipe[0] };
  // console.log(" thisRecipe ", thisRecipe);

  return (
    <div>
      <h2>{currRecipe[0].title}</h2>
    </div>
  );
}

export default RecipeDetail;

Note the use of filter above which returns an array. We can spread the array into a new variable and use <h2>{thisRecipe.title}</h2>.

Recipe Detail

Add a 'Home' link to RecipeDetail.js and flesh out the return value:

import React from "react";
import { Link, useParams } from "react-router-dom";

function RecipeDetail(props) {
  const { recipeId } = useParams();
  const currRecipe = props.recipes.filter((recipe) => recipe._id === recipeId);
  const thisRecipe = { ...currRecipe[0] };

  return (
    <div>
      <img src={`/img/${thisRecipe.image}`} alt={thisRecipe.title} />
      <h1>{thisRecipe.title}</h1>
      <p>{thisRecipe.description}</p>
      <Link to="/">Home</Link>
    </div>
  );
}

export default RecipeDetail;

Custom Hooks

Building custom Hooks lets you extract component logic into reusable functions.

Create a hooks directory and save this as useToggle.js:

import { useState } from "react";

function useToggle(initialVal = false) {
  // call useState, "reserve piece of state"
  const [state, setState] = useState(initialVal);
  const toggle = () => {
    setState(!state);
  };
  // return piece of state AND a function to toggle it
  return [state, toggle];
}

export default useToggle;

Demo the hook with Toggle.js in index.js:

import useToggle from "./hooks/useToggle";

function Toggler() {
  const [isHappy, toggleIsHappy] = useToggle(true);
  const [isBanana, toggleIsBanana] = useToggle(true);

  return (
    <div>
      <h1 onClick={toggleIsHappy}>{isHappy ? "😄" : "😢"}</h1>
      <h1 onClick={toggleIsBanana}>{isBanana ? "🍌" : "👹"}</h1>
    </div>
  );
}

Create a hooks directory in src, move the custom hook with the changes below into it, and export it.

import React from "react";

export function useFetch(url) {
  const [data, setData] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    setLoading(true);
    fetch(url)
      .then((res) => res.json())
      .then((data) => {
        setData(data);
        setError(null);
        setLoading(false);
      })
      .catch((error) => {
        console.warn(error.message);
        setError("error loading data");
        setLoading(false);
      });
  }, [url]);

  return {
    loading,
    data,
    error,
  };
}
  • import the hook into App.js:
import { useFetch } from "./hooks/useFetch";
  • Remove the useEffect from App.js
  • Destructure useFetch's return values:
function App() {
  const { loading, data, error } = useFetch(`/api/recipes`);
  • Change the recipes route to pass data the the recipes component:
<Route path="/" element={<Recipes recipes={recipes} />} />
  • Finally, add the loading and error returns we used in the json placeholder example:
if (loading === true) {
  return <p>Loading</p>;
}

if (error) {
  return <p>{error}</p>;
}

Review: destructuring on MDN. Note the ability to re-assign variable names and how they differ for Arrays and Objects.

After resetting data to recipes ( data: recipes ), here is App.js in its entirety:

import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Recipes from "./Recipes";
import RecipeDetail from "./RecipeDetail";
import { useFetch } from "./hooks/useFetch";

function App() {
  const { loading, data: recipes, error } = useFetch(`/api/recipes`);

  if (loading === true) {
    return <p>Loading</p>;
  }

  if (error) {
    return <p>{error}</p>;
  }

  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Recipes recipes={recipes} />} />
        <Route path="/:recipeId" element={<RecipeDetail recipes={recipes} />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;

Adding a NavBar

Create Nav.js:

import React from "react";

import { Link } from "react-router-dom";

const Nav = ({ loggedin, setLoggedin }) => {
  return (
    <nav>
      <h1>
        <Link to="/">Recipes</Link>
      </h1>

      {loggedin ? (
        <button onClick={() => setLoggedin(false)}>Log Out</button>
      ) : (
        <button onClick={() => setLoggedin(true)}>Log In</button>
      )}
    </nav>
  );
};

export default Nav;

Import it and render in App.js:

import React from "react";
import { BrowserRouter as Router, Route, Routes } from "react-router-dom";
import Recipes from "./Recipes";
import RecipeDetail from "./RecipeDetail";
import Nav from "./Nav";
import { useFetch } from "./hooks/api";

function App() {
  const [loggedin, setLoggedin] = React.useState(false);
  const { loading, data: recipes, error } = useFetch(`/api/recipes`);

  if (loading === true) {
    return <p>Loading</p>;
  }

  if (error) {
    return <p>{error}</p>;
  }

  return (
    <main>
      <BrowserRouter>
        <Nav setLoggedin={setLoggedin} loggedin={loggedin} />
        <Routes>
          <Route path="/" element={<Recipes recipes={recipes} />} />
          <Route
            path="/:recipeId"
            element={<RecipeDetail recipes={recipes} />}
          />
        </Routes>
      </BrowserRouter>
    </main>
  );
}

export default App;

npm i styled-components

Install styled components and create some css to support the new element:

import styled from "styled-components";

const NavStyles = styled.nav`
  min-height: 3rem;
  background-color: #007eb6;
  margin-bottom: 1rem;
  display: flex;
  flex-direction: row;
  align-content: center;
  justify-content: space-between;
  a {
    color: #fff;
    padding: 1rem;
    font-size: 2rem;
    text-decoration: none;
  }
  button {
    color: #fff;
    font-size: 1rem;
    padding: 0.5rem;
    margin: 0 1rem;
    background: #007eb6;
    border: 2px solid #fff;
    border-radius: 3px;
    align-self: center;
  }
`;

Creating a Reusable Button Component

An element shouldn’t set its width, margin, height and color. These attributes should be set by its parent(s).

Remove the button styles in Nav and review CSs variables by using a variable to set the background color of Nav:

const NavStyles = styled.nav`
  --bg-color: #007eb6;
  min-height: 3rem;
  background-color: var(--bg-color);
  margin-bottom: 1rem;
  display: flex;
  flex-direction: row;
  align-content: center;
  justify-content: space-between;
  a {
    color: #fff;
    padding: 1rem;
    font-size: 2rem;
    text-decoration: none;
  }
`;

Create a Button component in src/button/Button.js:

import React from "react";
import styled from "styled-components";

const StyledButton = styled.button`
  --btn-bg: var(--btn-color, #bada55);
  color: #fff;
  font-size: 1rem;
  padding: 0.5rem;
  margin: 0 1rem;
  background: var(--btn-bg);
  border: 2px solid #fff;
  border-radius: 3px;
  align-self: center;
  cursor: pointer;
`;

export default function Button({ children, func }) {
  return <StyledButton onClick={func}>{children}</StyledButton>;
}

Note: --btn-bg: var(--btn-color, #bada55); uses a variable to set a variable and provides a fallback.

Import it into Nav import Button from "./button/Button"; and compose it:

{ loggedin ? (
    <Button func={() => setLoggedin(false)}>Log Out</Button>
  ) : (
    <Button func={() => setLoggedin(true)}>Log In</Button>
  )
}

Set it to use a color variable passed in from Nav:

const NavStyles = styled.nav`
  --bg-color: #007eb6;
  --btn-color: #007eb6;

--btn-color: #007eb6; overrides the default in our Button component:

--btn-bg: var(--btn-color, #bada55);

we proably want to store our color palette at a higher level. Add to index.css:

:root {
  --blue-dark: #046e9d;
}

In Nav:

--btn-color: var(--blue-dark);

This is the beginning of our standalone Button component.

Also: const [loggedin, setLoggedin] = useToggle(false);

Adding a Recipe

Create FormCreateRecipe.js:

import React from "react";

const FormCreateRecipe = () => {
  return (
    <div>
      <h3>Add Recipe Form</h3>
      <form>
        <input type="text" placeholder="Recipe name" />
        <input type="text" placeholder="Recipe image" />
        <textarea type="text" placeholder="Recipe description" />
        <button type="submit">Add Recipe</button>
      </form>
    </div>
  );
};

export default FormCreateRecipe;

Allow it to render only if the user is logged in.

  • pass the logged in state from App.js to the Recipes component:
<Route path="/" element={<Recipes recipes={recipes} loggedin={loggedin} />} />

Import the component into Recipes.js:

import React from "react";
import Recipe from "./Recipe";
import FormCreateRecipe from "./FormCreateRecipe";

function Recipes({ recipes, loggedin }) {
  return (
    <section>
      {loggedin && <FormCreateRecipe />}
      {recipes.map((recipe) => (
        <Recipe key={recipe._id} recipe={recipe} />
      ))}
    </section>
  );
}

export default Recipes;

Add values state, handleInputchange and createRecipe functions to the form:

import React from "react";
import Button from "./button/Button";

const FormCreateRecipe = () => {
  const [values, setValues] = React.useState({
    title: "Recipe Title",
    image: "toast.png",
    description: "Description of the recipe",
  });

  const createRecipe = (event) => {
    event.preventDefault();
    const recipe = {
      title: values.title,
      image: values.image,
      description: values.description,
      year: values.year,
    };
    console.log(" making a recipe ", recipe);
  };

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    console.log(" name:: ", name, " value:: ", value);
    setValues({ ...values, [name]: value });
  };

  return (
    <div>
      <h3>Add Recipe Form</h3>
      <form onSubmit={createRecipe}>
        <input
          type="text"
          placeholder="Recipe title"
          value={values.title}
          name="title"
          onChange={handleInputChange}
        />
        <input
          type="text"
          placeholder="Recipe image"
          value={values.image}
          name="image"
          onChange={handleInputChange}
        />
        <textarea
          placeholder="Recipe description"
          name="description"
          onChange={handleInputChange}
          value={values.description}
        />
        <input
          type="text"
          placeholder="Recipe year"
          value={values.year}
          name="year"
          onChange={handleInputChange}
        />

        <Button type="submit">Add Recipe</Button>
      </form>
    </div>
  );
};

export default FormCreateRecipe;

Note the difference between what we are doing here for handling state change for inputs vs. how we accomplished the same task in previous classes. (Examine the console output.)

We are using computed property names.

Review Object assignment and computed values:

var testObj = {};

// dot assignment
testObj.age = 80;
console.log(testObj);

var myKey = "name";
var myValue = "Daniel";
testObj = {};

// bracket assignment
testObj[myKey] = myValue;
console.log(testObj);

// Computed Property Names
// Before: create the object first, then use bracket notation to assign that property to the value
function objectify(key, value) {
  let obj = {};
  obj[key] = value;
  return obj;
}

objectify("name", "Daniel"); //?

// After: use object literal notation to assign the expression as a property on the object without having to create it first
function objectifyTwo(key, value) {
  return {
    [key]: value,
  };
}

objectifyTwo("color", "purple"); //?

Test the button.

Adding a Recipe

A New Custom Hook

const defaultHeaders = {
  "Content-Type": "application/json",
  Accept: "application/json",
};

async function fetchData({ path, method, data, headers }) {
  const response = await fetch(path, {
    method: method,
    body: !!data ? JSON.stringify(data) : null,
    headers: !!headers ? headers : defaultHeaders,
  }).then((response) => response.json());
  return response;
}

export function useApi() {
  return {
    get: (path, headers) =>
      fetchData({
        path: path,
        method: "GET",
        data: null,
        headers: headers,
      }),
    post: (path, data, headers) =>
      fetchData({
        path: path,
        method: "POST",
        data: data,
        headers: headers,
      }),
    put: (path, data, headers) =>
      fetchData({
        path: path,
        method: "PUT",
        data: data,
        headers: headers,
      }),
    del: (path, headers) =>
      fetchData({
        path: path,
        method: "DELETE",
        data: null,
        headers: headers,
      }),
  };
}

export default useApi;

We need to alter App.js to use the new hook:

import React from "react";
import Recipes from "./Recipes";
import RecipeDetail from "./RecipeDetail";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useFetch } from "./hooks/useFetch";
import Nav from "./Nav";
import useToggle from "./hooks/useToggle";

function App() {
  const [recipes, setRecipes] = React.useState([]);
  const [loggedin, setLoggedin] = useToggle(true);
  const [loading, setLoading] = useToggle(true);
  const [error, setError] = React.useState("");
  const { get } = useFetch(`/api/recipes`);

  /* eslint-disable react-hooks/exhaustive-deps */
  React.useEffect(() => {
    setLoading(true);
    get("/api/recipes")
      .then((data) => {
        setRecipes(data);
        setLoading(false);
      })
      .catch((error) => setError(error));
  }, []);

  if (loading === true) {
    return <p>Loading</p>;
  }

  if (error) {
    return <p>{error}</p>;
  }

  return (
    <main>
      <BrowserRouter>
        <Nav setLoggedin={setLoggedin} loggedin={loggedin} />
        <Routes>
          <Route
            path="/"
            element={<Recipes recipes={recipes} loggedin={loggedin} />}
          />
          <Route
            path="/:recipeId"
            element={<RecipeDetail recipes={recipes} />}
          />
        </Routes>
      </BrowserRouter>
    </main>
  );
}

export default App;

addRecipe Function

Add an addRecipe function to App.js and props drill it down to Recipes:

  const { get, post } = useFetch(`/api/recipes`);
...

  const addRecipe = (recipe) => {
    post("/api/recipes", recipe).then((data) => {
      setRecipes([data, ...recipes]);
    });
  };

...

<Route
  path="/"
  element={
    <Recipes
      recipes={recipes}
      loggedin={loggedin}
      addRecipe={addRecipe}
    />
  }
/>

Note the addition of post from the useFetch custom hook.

Props drill the addRecipe function to the form:

import React from "react";
import Recipe from "./Recipe";
import FormCreateRecipe from "./FormCreateRecipe";

function Recipes({ recipes, loggedin, addRecipe }) {
  return (
    <section>
      {loggedin && <FormCreateRecipe addRecipe={addRecipe} />}
      {recipes.map((recipe) => (
        <Recipe key={recipe._id} recipe={recipe} />
      ))}
    </section>
  );
}

export default Recipes;

In FormCreateRecipe, destructure addRecipe and call it with a recipe:

const FormCreateRecipe = ({ addRecipe }) => {
  const [values, setValues] = React.useState({
    title: "Recipe Title",
    image: "toast.png",
    description: "Description of the recipe",
    year: "2021"
  });

  const createRecipe = (event) => {
    event.preventDefault();
    const recipe = {
      title: values.title,
      image: values.image,
      description: values.description,
      year: values.year,
    };
    addRecipe(recipe);
  };

Test and debug.

Demo - backend validation:

const RecipeSchema = new mongoose.Schema({
  title: String,
  description: String,
  image: String,
  // year: String,
  year: {
    type: String,
    required: true,
  },
});
const [values, setValues] = React.useState({
  title: "",
  image: "",
  description: "",
  year: "",
});

Delete

In App.js:

const { get, post, del } = useFetch(`/api/recipes`);
...

  const deleteRecipe = (recipeId) => {
    console.log("recipeId:", recipeId);
    del(`/api/recipes/${recipeId}`).then(window.location.replace("/"));
  };
  ...

<Route
  path="/:recipeId"
  element={
    <RecipeDetail
      recipes={recipes}
      deleteRecipe={deleteRecipe}
      loggedin={loggedin}
    />
  }
/>

RecipeDetail.js:

{props.loggedin && (
  <button onClick={() => props.deleteRecipe(thisRecipe._id)}>
    delete
  </button>
)}

<Link to="/">Home</Link>;

App.js:

const deleteRecipe = (recipeId) => {
  console.log("recipeId:", recipeId);
  del(`/api/recipes/${recipeId}`).then(
    setRecipes((recipes) => recipes.filter((recipe) => recipe._id !== recipeId))
  );
};

Destructure all props and add a piece of state to determine the view:

import React from "react";
import { Link, useParams } from "react-router-dom";

function RecipeDetail({ recipes, loggedin, deleteRecipe }) {
  const { recipeId } = useParams();
  const [recipeDeleted, setRecipeDeleted] = React.useState(false);

  const currRecipe = recipes.filter((recipe) => recipe._id === recipeId);
  const thisRecipe = { ...currRecipe[0] };

  const delRecipe = () => {
    deleteRecipe(thisRecipe._id);
    setRecipeDeleted(true);
  };

  if (recipeDeleted) {
    return (
      <>
        <p>Recipe deleted!</p>
        <Link to="/">Home</Link>
      </>
    );
  }

  return (
    <div>
      <img src={`/img/${thisRecipe.image}`} alt={thisRecipe.title} />
      <h1>{thisRecipe.title}</h1>
      <p>{thisRecipe.description}</p>

      {loggedin && <button onClick={() => delRecipe()}>delete</button>}

      <Link to="/">Home</Link>
    </div>
  );
}

export default RecipeDetail;

Update a Recipe

App.js:

const { get, post, del, put } = useFetch(`/api/recipes`);
...
const editRecipe = (updatedRecipe) => {
  console.log(updatedRecipe);
  put(`/api/recipes/${updatedRecipe._id}`, updatedRecipe).then(
    get("/api/recipes").then((data) => {
      setRecipes(data);
    })
  );
};
...
<RecipeDetail
  recipes={recipes}
  deleteRecipe={deleteRecipe}
  loggedin={loggedin}
  editRecipe={editRecipe}
/>

New src/FormEditRecipe.js:

import React from "react";
import Button from "./button/Button";

const FormEditRecipe = ({ editRecipe, thisRecipe }) => {
  const [values, setValues] = React.useState({
    title: thisRecipe.title,
    image: thisRecipe.image,
    description: thisRecipe.description,
    year: thisRecipe.year,
  });

  const updateRecipe = (event) => {
    event.preventDefault();
    const recipe = {
      ...thisRecipe,
      title: values.title,
      image: values.image,
      description: values.description,
      year: values.year,
    };
    editRecipe(recipe);
  };

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    console.log(" name:: ", name, " value:: ", value);
    setValues({ ...values, [name]: value });
  };

  return (
    <div>
      <h3>Edit Recipe</h3>
      <form onSubmit={updateRecipe}>
        <input
          type="text"
          placeholder="Recipe title"
          value={values.title}
          name="title"
          onChange={handleInputChange}
        />
        <input
          type="text"
          placeholder="Recipe image"
          value={values.image}
          name="image"
          onChange={handleInputChange}
        />
        <textarea
          placeholder="Recipe description"
          name="description"
          onChange={handleInputChange}
          value={values.description}
        />
        <input
          type="text"
          placeholder="Recipe year"
          value={values.year}
          name="year"
          onChange={handleInputChange}
        />

        <Button type="submit">Edit Recipe</Button>
      </form>
    </div>
  );
};

export default FormEditRecipe;

Compose it inRecipeDetail.js:

import FormEditRecipe from "./FormEditRecipe";
...
function RecipeDetail({ recipes, loggedin, deleteRecipe, editRecipe }) {
...
<FormEditRecipe thisRecipe={thisRecipe} editRecipe={editRecipe} />

Context

Context provides a way to pass data through the component tree without having to pass props down manually at every level. - The React Docs

Create src/RecipesContext.js:

import React from "react";

const RecipesContext = React.createContext();

export default RecipesContext;

App.js

import RecipesContext from "./RecipesContext";
...
<RecipesContext.Provider value={recipes}>
  <main>
    <BrowserRouter>
      <Nav setLoggedin={setLoggedin} loggedin={loggedin} />
      <Routes>
        {/* NOTE - we no longer pass recipes as a prop to Recipes */}
        <Route
          path="/"
          element={<Recipes loggedin={loggedin} addRecipe={addRecipe} />}
        />
        <Route
          path="/:recipeId"
          element={
            <RecipeDetail
              recipes={recipes}
              deleteRecipe={deleteRecipe}
              loggedin={loggedin}
              editRecipe={editRecipe}
            />
          }
        />
      </Routes>
    </BrowserRouter>
  </main>
</RecipesContext.Provider>

Recipes.js

import React from "react";
import Recipe from "./Recipe";
import FormCreateRecipe from "./FormCreateRecipe";
import RecipesContext from "./RecipesContext";

function Recipes({ loggedin, addRecipe }) {
  const recipes = React.useContext(RecipesContext);
  return (
    <section>
      {loggedin && <FormCreateRecipe addRecipe={addRecipe} />}
      {recipes.map((recipe) => (
        <Recipe key={recipe._id} recipe={recipe} />
      ))}
    </section>
  );
}

export default Recipes;

RecipeDetail.js

import RecipesContext from "./RecipesContext";

function RecipeDetail({ loggedin, deleteRecipe, editRecipe }) {
  const recipes = React.useContext(RecipesContext);
  ...

Notes

Deployment

const { createProxyMiddleware } = require("http-proxy-middleware");

module.exports = function (app) {
  app.use(
    createProxyMiddleware(["/api", "/img"], {
      target: "http://localhost:3456/",
    })
  );
};

//   "proxy": "https://recipes-do-not-delete.herokuapp.com/",
//   "proxy": "http://localhost:3456/",

We could run npm run build in the client folder and use that or use a Heroku postbuild script.

In the script section of server's package.json add a script to:

  1. turn production mode off (you can't run npm build in the production environment)
  2. do an npm install and build (prefixing client)
"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"

server.js needs to be set up to serve the build.

Make sure the following goes after all the routes:

if (process.env.NODE_ENV === "production") {
  // set static folder
  app.use(express.static("client/build"));
  app.get("*", (req, res) => {
    res.sendFile(path.resolve(__dirname, "client", "build", "index.html"));
  });
}

The path.resolve has to have four arguments.

Because we're using Node's built in path method be sure to require it (at the top of server.js):

const path = require('path');

And also... be sure to remove the old express.static middleware (it is now in the 'else' statement above).

You can also add your database URI to Config Vars in the Heroku Settings for your project.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 74.9%
  • HTML 19.1%
  • CSS 6.0%