I have just been introduced to Next.js and have been tasked to create a dynamic website that uses data retrieved from an API. The web app should contain at least two pages: an index page and a page that displays details about the topic that the user selects on the index page.
I have chosen to make use of the Edamam recipe API and to use the search functionality on the index/ home page to render the recipe results on the page to fulfil the brief. I am, however, experiencing some trouble iterating over the data.
Please see below the error:
My code is as follows:
- Pages:
index.js
// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// import { Spinner } from "@chakra-ui/react";
// Imported AppDisplay to set the holistic style of this page.
import AppDisplay from "../components/AppDisplay";
// Imported Carousel from React Bootstrap.
import { Carousel } from "react-bootstrap";
// Importing the SearchForm component.
import SearchForm from "../components/SearchForm";
/**
* Styled the home page.
*/
const carouselStyle = {
overflowX: "hidden",
overflowY: "hidden",
height: "auto",
width: "auto",
};
const logoStyle = {
height: "450px",
width: "auto",
marginBottom: "70px",
};
/**
* Applied the styles and passed the AppDisplay props.
* @returns Styled home page, displaying a styled introduction header text section an image and a header component.
*/
const Home = (props) => {
const { search, onInputChange } = props;
return (
<div>
<AppDisplay>
<div>
<Carousel variant="dark" style={carouselStyle}>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Breakfast.jpg"
alt="First slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Dinner.jpg"
alt="Second slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Dessert.jpg"
alt="Third slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Bake.jpg"
alt="Third slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Burger.jpg"
alt="Third slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Casserole.jpg"
alt="Third slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Pizza.jpg"
alt="Third slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
</Carousel.Caption>
</Carousel.Item>
<Carousel.Item>
<img
className="d-block w-100"
src="/static/images/Pudding.jpg"
alt="Third slide"
/>
<Carousel.Caption>
<img
src="/static/images/GrumbleLogoMain.png"
alt="Grumble Logo"
style={logoStyle}
/>
<SearchForm value={search} onChange={onInputChange} />
<div id="edamam-badge" data-color="white" z-index="1"></div>
</Carousel.Caption>
</Carousel.Item>
</Carousel>
{/* <div id="edamam-badge" data-color="white"></div> */}
</div>
</AppDisplay>
</div>
);
};
// Exported home page to be generated.
export default Home;
recipes.js
// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Imported AppDisplay to set the holistic style of this page.
import AppDisplay from "../components/AppDisplay";
// Imported Recipe component.
import RecipeData from "../components/RecipeData";
import Header from "../components/Header";
const Recipes = (props) => {
const { recipes } = props;
console.log("props:", props);
const recipeDetails = recipes.map(({ recipe }) => ({
label: recipe.recipe.label,
source: recipe.recipe.source,
totalTime: recipe.recipe.totalTime,
cuisineType: recipe.recipe.cuisineType,
mealType: recipe.recipe.mealType,
healthLabels: recipe.recipe.healthLabels,
dietLabels: recipe.recipe.dietLabels,
image: recipe.recipe.image,
ingredientLines: recipe.recipe.ingredientLines,
url: recipe.recipe.url,
}));
return (
<div>
<AppDisplay />
<Header />
<div>
{recipeDetails.map((recipes) => (
<RecipeData recipes={recipes} />
))}
</div>
</div>
);
};
// Exported home page to be generated.
export default Recipes;
- Components:
Header.js
// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Imported Font Awesome library and icons. Also added "import "@fortawesome/fontawesome-svg-core/styles.css";" to allow styling the icons.
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faHome } from "@fortawesome/free-solid-svg-icons";
import "@fortawesome/fontawesome-svg-core/styles.css";
/**
* Styled the header component.
*/
// Setting the header's position to absolute and set the padding and background color to transparent.
const headerStyle = {
// position: "absolute",
height: "auto",
width: "auto",
display: "flex",
flexDirection: "row",
padding: 5,
backgroundColor: "#393d49",
zIndex: 1,
};
// Set the size (height x width) of the header's logo.
const logoStyle = {
height: "80px",
width: "auto",
};
// Set the margins and the font color, size and decoration of the header links.
const linkStyle = {
margin: "auto 40px auto 20px",
color: "#ffffff",
fontSize: 20,
textDecoration: "none",
};
// Set the recipe page's visibility to hidden.
const recipeLinkStyle = {
visibility: "hidden",
};
// Created onMouseOver and onMouseOut event handler functions to change the font colors of the header links once hovered
// over and to change back the colour when the links are no longer hovered over.
const changeFontColor = (e) => {
e.target.style.color = "#f1b374";
};
const changeBackFontColor = (e) => {
e.target.style.color = "#ffffff";
};
// Set the font size and the right margin of the home icon.
const iconStyle = {
fontSize: "1.1rem",
marginRight: "5px",
color: "#ffffff",
};
/**
* Attached the event handlers to the links with onMouseOver and onMouseOut.
* @returns The styled header component with navigatable, styled links.
*/
const Header = () => (
<div style={headerStyle}>
<img
src="/static/images/GrumbleLogoHead.png"
alt="Grumble Logo"
style={logoStyle}
/>
<Link href="/">
<a
style={linkStyle}
onMouseOver={changeFontColor}
onMouseOut={changeBackFontColor}
>
<FontAwesomeIcon icon={faHome} style={iconStyle} />
Home
</a>
</Link>
<Link href="/recipes">
<a style={recipeLinkStyle}>RECIPES</a>
</Link>
</div>
);
// Exporting the Header to the recipe page.
export default Header;
AppDisplay.js
// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Importing the Next built-in component to append elements to the head of the page.
import Head from "next/head";
/**
* Created a global style.
*/
// Set the application's margins, padding and font size and families. Also set for the vertical and horizontal overflow to be hidden.
const appDisplayStyle = {
margin: 0,
padding: 0,
overflowX: "hidden",
overflowY: "hidden",
fontSize: 15,
fontFamily: "Staatliches, Trebuchet, Helvetica",
};
/**
* Added the links to utilize React Bootstrap and the Google font.
* @param {*} props Children pages for appDisplayStyle to render - index, recipes.
* @returns The application's general styling, with appended links, for use in the pages.
*/
const AppDisplay = (props) => (
<div>
<Head>
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
crossOrigin="anonymous"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css?family=Staatliches"
/>
{/* <script src="https://developer.edamam.com/attribution/badge.js"></script> */}
</Head>
<div style={appDisplayStyle}>{props.children}</div>
</div>
);
// Exporting AppDisplay for use on the pages.
export default AppDisplay;
SearchForm.js
// Imported the Link API to support client-side navigation.
import Link from "next/Link";
// Imported React library and hooks.
import { useEffect, useState } from "react";
// Requiring Axios.
import axios from "axios";
// Imported components from React Bootstrap.
import { Form, FormControl } from "react-bootstrap";
// Imported Font Awesome library and icons. Also added "import "@fortawesome/fontawesome-svg-core/styles.css";" to allow styling the icons.
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSearch } from "@fortawesome/free-solid-svg-icons";
import "@fortawesome/fontawesome-svg-core/styles.css";
/**
* Styled the SearchForm component.
*/
// Set the search container's position to absolute and aligned it to the top and left. Also set the left margin to counter the left position.
const searchContainer = {
position: "absolute",
top: "68%",
left: "45.5%",
marginLeft: "-100px",
};
// Set for the form container to display as flex and the direction to row. Also set the position to relative to allow the icon to appear inside
// the input area.
const formContainer = {
display: "flex",
flexDirection: "row",
position: "relative",
};
// Set the size (height x width), the padding and the background color of the input element.
const searchInputStyle = {
height: "35px",
width: 300,
padding: 5,
backgroundColor: "#ffffff",
};
// Set the icon's position to absolute and aligned it to the top and left. Also set the height, the font size and color and for the cursor to
// to a pointer once it hovers over the icon.
const iconStyle = {
position: "absolute",
left: "275px",
top: "8px",
height: "20px",
fontSize: "1rem",
color: "#808080",
cursor: "pointer",
};
const SearchForm = () => {
const [recipes, setRecipes] = useState([]);
const [search, setSearch] = useState("");
console.log("recipes:", recipes);
const API_ID = "some_sensitive_data";
const API_KEY = "some_sensitive_data";
useEffect(() => {
sendApiRequest();
return () => {
setRecipes({});
};
}, []);
// An asynchronous function fetching data from the API.
const sendApiRequest = async () => {
const res = await axios.get(
// `https://api.edamam.com/search?q=${search}&app_id=${API_ID}&app_key=${API_KEY}`
`https://api.edamam.com/search?q=bacon&app_id=${API_ID}&app_key=${API_KEY}&from=0&to=12`
);
// const data = await res.json();
setRecipes(res.data.hits);
console.log("res.data.hits:", res.data.hits);
};
const onInputChange = (e) => {
setSearch();
console.log(e.target.value);
};
return (
<div>
<div style={searchContainer}>
<Form
className="search-form"
style={formContainer}
onSubmit={sendApiRequest}
>
<FormControl
type="text"
placeholder="Search"
className="search-bar mr-sm-2"
style={searchInputStyle}
onChange={onInputChange}
value={search}
// isDisabled={isLoading}
/>
<a href="/recipes">
<FontAwesomeIcon
icon={faSearch}
style={iconStyle}
type="submit"
className="search-button"
id="search"
onClick={sendApiRequest}
/>
</a>
</Form>
</div>
</div>
);
};
// Exported the RecipeListings to SearchForm.
export default SearchForm;
RecipeData.js
// Imported React library and hooks.
// import { useEffect, useState } from "react";
import { Card, Button } from "react-bootstrap";
const RecipeData = (props) => {
console.log('props:', props)
const {
label,
source,
totalTime,
cuisineType,
mealType,
healthLabels,
dietLabels,
image,
ingredientLines,
url,
} = props;
return (
<Card col-3 offset-1>
<Card.Header>
<h5>{label}</h5>
<table>
<tr>
<th>Recipe By:</th>
<td>{source}</td>
</tr>
<tr>
<th>Preparation Time:</th>
<td>{totalTime}</td>
</tr>
<tr>
<th>Cuisine:</th>
<td>{cuisineType}</td>
</tr>
<tr>
<th>Meal Type:</th>
<td>{mealType}</td>
</tr>
<tr>
<th>Health:</th>
<td>{healthLabels}</td>
</tr>
<tr>
<th>Dietary Information:</th>
<td>{dietLabels}</td>
</tr>
</table>
</Card.Header>
<Card.Img src={image} alt="Recipe Photograph" />
<Card.Body>
<ul>
{ingredientLines.map((ingredients) => (
<li>{ingredients}</li>
))}
</ul>
</Card.Body>
<Card.Footer>
<Button href={url} target="_blank">
Method and More
</Button>
</Card.Footer>
</Card>
);
};
// Exported recipeDetails to be generated.
export default RecipeData;
I have run console.logs on recipes (SearchForm.js - empty array returned), props (RecipeData.js - empty object returned) and on res.data.hits (SearchForm.js - returned data).
I seem to be having trouble defining the props in the pages/ components, but am not having any success sorting it out.
I would appreciate it if anyone is willing to assist.