In the culinary world, reviews and ratings are the secret sauce. They guide us to the best dishes, help us avoid kitchen disasters, and foster a sense of community among food lovers. Imagine a world where you can quickly rate your favorite recipes, share your experiences, and discover new culinary gems based on the wisdom of others. That’s the power we’ll be harnessing today as we build a recipe rating application using Next.js.
This tutorial is designed for developers who are familiar with the basics of JavaScript, HTML, and CSS, and have a foundational understanding of React. We’ll delve into the world of Next.js, a powerful framework that simplifies building modern web applications. By the end of this guide, you’ll have a fully functional recipe rating app, complete with the ability to add, view, and rate recipes. This project will not only sharpen your Next.js skills but also equip you with the knowledge to build interactive and engaging web applications.
Why Build a Recipe Rating App?
Recipe rating applications are more than just a fun project; they offer several practical benefits:
- Practical Skill Application: This project gives you hands-on experience with core web development concepts, including data fetching, state management, and user interface design.
- Real-World Relevance: Recipe rating apps have a broad appeal, and the techniques you learn can be applied to various projects, from e-commerce sites to social platforms.
- Learning Next.js: You’ll learn the ins and outs of Next.js features like server-side rendering, API routes, and dynamic routing, which are essential for building performant and SEO-friendly web applications.
- Portfolio Enhancement: A completed project is a great addition to your portfolio, showcasing your ability to create interactive and user-friendly web experiences.
Let’s get started on building a recipe rating app that’s both functional and fun!
Setting Up Your Next.js Project
Before we dive into the code, we need to set up our Next.js project. If you haven’t already, make sure you have Node.js and npm (or yarn) installed on your system. Open your terminal and run the following command to create a new Next.js project:
npx create-next-app recipe-rating-app
This command will create a new directory called recipe-rating-app with all the necessary files to get started. Navigate into your project directory:
cd recipe-rating-app
Now, let’s install some dependencies that we’ll need for our project. We’ll use a library called react-icons for easy access to icons and uuid to generate unique IDs for our recipes and ratings:
npm install react-icons uuid
After the installation is complete, you can start the development server by running:
npm run dev
This command will start the development server, and you can access your app in your browser at http://localhost:3000. Now, let’s move on to the next step: designing the app’s structure.
Designing the App Structure
Before we start coding, let’s plan the structure of our app. We’ll break down the app into several components:
- RecipeList: This component will display a list of recipes.
- RecipeCard: Each recipe in the list will be rendered using this component. It will show the recipe details and the rating.
- RecipeForm: This component will be used to add new recipes.
- RatingStars: A reusable component to display and allow users to rate recipes.
We will also use the following pages:
- index.js (/) This will be our main page, displaying the recipe list.
- [id].js (/recipe/[id]) This will be a dynamic route to show the recipe details and ratings.
Creating the RecipeList Component
Let’s start by creating the RecipeList component. This component will fetch and display a list of recipes. Create a new file named RecipeList.js in the components directory (you may need to create this directory if it doesn’t exist):
// components/RecipeList.js
import React from 'react';
import RecipeCard from './RecipeCard';
function RecipeList({ recipes }) {
return (
<div className="recipe-list">
{
recipes.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} />
))
}
</div>
);
}
export default RecipeList;
In this component, we map over the recipes array (which we’ll pass as a prop later) and render a RecipeCard component for each recipe. The key prop is important for React to efficiently update the list.
Building the RecipeCard Component
Next, let’s create the RecipeCard component. This component will display the details of a single recipe. Create a new file named RecipeCard.js in the components directory:
// components/RecipeCard.js
import React from 'react';
import { FaStar } from 'react-icons/fa';
function RecipeCard({ recipe }) {
return (
<div className="recipe-card">
<h3>{recipe.name}</h3>
<p>{recipe.description}</p>
<div className="recipe-rating">
{/* Add RatingStars component here */}
<p>Rating: {recipe.rating} <FaStar /></p>
</div>
</div>
);
}
export default RecipeCard;
This component displays the recipe’s name, description, and rating. We’re also importing the FaStar icon from react-icons to display a star next to the rating. We’ll add the RatingStars component later to handle the rating functionality.
Creating the RatingStars Component
Now, let’s create the RatingStars component, which will allow users to give ratings to recipes. Create a new file named RatingStars.js in the components directory:
// components/RatingStars.js
import React, { useState } from 'react';
import { FaStar } from 'react-icons/fa';
function RatingStars({ rating, onRatingChange }) {
const [hover, setHover] = useState(null);
return (
<div className="star-rating">
{[...Array(5)].map((star, index) => {
const ratingValue = index + 1;
return (
<label key={index}>
<input
type="radio"
name="rating"
value={ratingValue}
onClick={() => onRatingChange(ratingValue)}
/>
<FaStar
className="star"
color={ratingValue <= (hover || rating) ? "#ffc107" : "#e4e5e9"}
size={24}
onMouseEnter={() => setHover(ratingValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
);
})}
</div>
);
}
export default RatingStars;
This component renders five stars. Users can click on a star to rate a recipe. The component uses the FaStar icon from react-icons. We use the useState hook to manage the hover state and the selected rating. We also pass a function onRatingChange, which is called when a star is clicked, allowing the parent component to handle the rating.
Building the RecipeForm Component
Now let’s build the RecipeForm component. This component will allow users to add new recipes. Create a new file named RecipeForm.js in the components directory:
// components/RecipeForm.js
import React, { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
function RecipeForm({ onAddRecipe }) {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
const newRecipe = {
id: uuidv4(),
name: name,
description: description,
rating: 0,
};
onAddRecipe(newRecipe);
setName('');
setDescription('');
};
return (
<form onSubmit={handleSubmit} className="recipe-form">
<h3>Add a Recipe</h3>
<div>
<label htmlFor="name">Recipe Name:</label>
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="description">Description:</label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
required
/>
</div>
<button type="submit">Add Recipe</button>
</form>
);
}
export default RecipeForm;
This component uses the useState hook to manage the form inputs. When the form is submitted, it calls the onAddRecipe function (passed as a prop) with the new recipe data. We’re using the uuid library to generate unique IDs for each recipe.
Creating the Main Page (index.js)
Now, let’s create the main page of our application, which will display the list of recipes and the recipe form. Open the pages/index.js file (or create it if it doesn’t exist) and add the following code:
// pages/index.js
import React, { useState, useEffect } from 'react';
import RecipeList from '../components/RecipeList';
import RecipeForm from '../components/RecipeForm';
function Home() {
const [recipes, setRecipes] = useState([]);
useEffect(() => {
// Load recipes from localStorage on component mount
const storedRecipes = localStorage.getItem('recipes');
if (storedRecipes) {
setRecipes(JSON.parse(storedRecipes));
}
}, []);
useEffect(() => {
// Save recipes to localStorage whenever the recipes state changes
localStorage.setItem('recipes', JSON.stringify(recipes));
}, [recipes]);
const handleAddRecipe = (newRecipe) => {
setRecipes([...recipes, newRecipe]);
};
return (
<div className="container">
<h1>Recipe Rating App</h1>
<RecipeForm onAddRecipe={handleAddRecipe} />
<RecipeList recipes={recipes} />
</div>
);
}
export default Home;
This is the main page component. It imports the RecipeList and RecipeForm components. It uses the useState hook to manage the list of recipes. The handleAddRecipe function is used to add new recipes to the list. We’re also using useEffect hooks to load and save recipes to localStorage, so the data persists even when the user refreshes the page. We will modify this component later to handle recipe details and ratings.
Styling Your Application
To make your application look good, you’ll need to add some CSS. Create a file named styles/globals.css in your project’s root directory (if it does not already exist) and add the following CSS rules:
/* styles/globals.css */
body {
font-family: sans-serif;
margin: 0;
padding: 0;
background-color: #f4f4f4;
}
.container {
max-width: 960px;
margin: 20px auto;
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
}
.recipe-form {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
}
.recipe-form div {
margin-bottom: 10px;
}
.recipe-form label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.recipe-form input[type="text"],
.recipe-form textarea {
width: 100%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.recipe-form button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.recipe-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.recipe-card {
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #f9f9f9;
}
.recipe-card h3 {
margin-top: 0;
color: #333;
}
.recipe-rating {
display: flex;
align-items: center;
margin-top: 10px;
}
.star-rating {
display: flex;
margin-right: 10px;
}
.star {
cursor: pointer;
}
To apply these styles, open pages/_app.js (or create it if it doesn’t exist) and import the CSS file:
// pages/_app.js
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp;
Adding Dynamic Recipe Details Page
Now, let’s create a dynamic route to show the recipe details and ratings. We’ll create a file named [id].js inside the pages/recipe directory. This file will handle the dynamic route /recipe/[id], where [id] is the unique identifier for a recipe. First, create the recipe directory inside the pages directory if you don’t already have it.
// pages/recipe/[id].js
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import RecipeCard from '../../components/RecipeCard';
import RatingStars from '../../components/RatingStars';
function RecipeDetails() {
const router = useRouter();
const { id } = router.query;
const [recipe, setRecipe] = useState(null);
useEffect(() => {
if (id) {
const storedRecipes = localStorage.getItem('recipes');
if (storedRecipes) {
const recipes = JSON.parse(storedRecipes);
const foundRecipe = recipes.find((recipe) => recipe.id === id);
setRecipe(foundRecipe);
}
}
}, [id]);
const handleRatingChange = (newRating) => {
if (recipe) {
const updatedRecipe = { ...recipe, rating: newRating };
// Update the recipe in localStorage
const storedRecipes = localStorage.getItem('recipes');
if (storedRecipes) {
const recipes = JSON.parse(storedRecipes);
const updatedRecipes = recipes.map((rec) =>
rec.id === id ? updatedRecipe : rec
);
localStorage.setItem('recipes', JSON.stringify(updatedRecipes));
setRecipe(updatedRecipe);
}
}
};
if (!recipe) {
return <p>Loading...</p>; // Or a "Recipe not found" message
}
return (
<div className="container">
<h1>{recipe.name}</h1>
<RecipeCard recipe={recipe} />
<div className="recipe-rating">
<RatingStars rating={recipe.rating} onRatingChange={handleRatingChange} />
</div>
</div>
);
}
export default RecipeDetails;
In this component:
- We use
useRouterfromnext/routerto get theidfrom the URL. - We fetch the recipe from
localStoragebased on theid. - We render the
RecipeCardcomponent to display the recipe details. - We integrate the
RatingStarscomponent for rating the recipe. - We update the rating in
localStoragewhen the user changes the rating.
Linking to Recipe Details
To make the app interactive, we need to link each recipe in the RecipeList to its details page. Update the RecipeCard component to include a link to the recipe details page. Import the Link component from next/link. Then, wrap the recipe name in a Link component.
// components/RecipeCard.js
import React from 'react';
import { FaStar } from 'react-icons/fa';
import Link from 'next/link';
function RecipeCard({ recipe }) {
return (
<div className="recipe-card">
<Link href={`/recipe/${recipe.id}`}>
<h3>{recipe.name}</h3>
</Link>
<p>{recipe.description}</p>
<div className="recipe-rating">
<p>Rating: {recipe.rating} <FaStar /></p>
</div>
</div>
);
}
export default RecipeCard;
Now, when you click on the recipe name, you’ll be taken to the recipe details page.
Integrating the RatingStars into RecipeCard
Now, let’s incorporate the RatingStars component into the RecipeCard to allow users to rate recipes directly from the list view. To implement this, modify the RecipeCard component to include the RatingStars component. Also, we will need to pass the rating and an onRatingChange function. Since we are storing the recipes in localStorage, we will need to update the recipe when a rating is changed.
// components/RecipeCard.js
import React from 'react';
import { FaStar } from 'react-icons/fa';
import Link from 'next/link';
import RatingStars from './RatingStars';
function RecipeCard({ recipe, onRatingChange }) {
return (
<div className="recipe-card">
<Link href={`/recipe/${recipe.id}`}>
<h3>{recipe.name}</h3>
</Link>
<p>{recipe.description}</p>
<div className="recipe-rating">
<RatingStars rating={recipe.rating} onRatingChange={(newRating) => onRatingChange(recipe.id, newRating)} />
</div>
</div>
);
}
export default RecipeCard;
Next, we need to update the RecipeList component to pass the onRatingChange function to the RecipeCard component. The onRatingChange function will be passed as a prop from index.js. We will also need to update the recipes in localStorage when a rating changes.
// pages/index.js
import React, { useState, useEffect } from 'react';
import RecipeList from '../components/RecipeList';
import RecipeForm from '../components/RecipeForm';
function Home() {
const [recipes, setRecipes] = useState([]);
useEffect(() => {
// Load recipes from localStorage on component mount
const storedRecipes = localStorage.getItem('recipes');
if (storedRecipes) {
setRecipes(JSON.parse(storedRecipes));
}
}, []);
useEffect(() => {
// Save recipes to localStorage whenever the recipes state changes
localStorage.setItem('recipes', JSON.stringify(recipes));
}, [recipes]);
const handleAddRecipe = (newRecipe) => {
setRecipes([...recipes, newRecipe]);
};
const handleRatingChange = (recipeId, newRating) => {
const updatedRecipes = recipes.map((recipe) =>
recipe.id === recipeId ? { ...recipe, rating: newRating } : recipe
);
setRecipes(updatedRecipes);
localStorage.setItem('recipes', JSON.stringify(updatedRecipes));
};
return (
<div className="container">
<h1>Recipe Rating App</h1>
<RecipeForm onAddRecipe={handleAddRecipe} />
<RecipeList recipes={recipes} onRatingChange={handleRatingChange} />
</div>
);
}
export default Home;
Finally, we need to update the RecipeList component to pass the onRatingChange function to the RecipeCard component.
// components/RecipeList.js
import React from 'react';
import RecipeCard from './RecipeCard';
function RecipeList({ recipes, onRatingChange }) {
return (
<div className="recipe-list">
{
recipes.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} onRatingChange={onRatingChange} />
))
}
</div>
);
}
export default RecipeList;
With these changes, you should now be able to rate recipes directly from the main page.
Error Handling and User Experience
While the app is functional, we can improve the user experience and robustness by adding error handling and some user-friendly features:
- Loading States: Display a loading indicator while fetching data or performing operations.
- Error Messages: Show informative error messages if something goes wrong.
- Input Validation: Validate user inputs in the recipe form to prevent invalid data from being saved.
- Empty State: Display a message when there are no recipes.
Let’s add a simple loading state to the RecipeDetails component. Modify the RecipeDetails component to show a “Loading…” message while the recipe is being fetched.
// pages/recipe/[id].js
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import RecipeCard from '../../components/RecipeCard';
import RatingStars from '../../components/RatingStars';
function RecipeDetails() {
const router = useRouter();
const { id } = router.query;
const [recipe, setRecipe] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
if (id) {
const storedRecipes = localStorage.getItem('recipes');
if (storedRecipes) {
const recipes = JSON.parse(storedRecipes);
const foundRecipe = recipes.find((recipe) => recipe.id === id);
setRecipe(foundRecipe);
}
}
setLoading(false);
}, [id]);
const handleRatingChange = (newRating) => {
if (recipe) {
const updatedRecipe = { ...recipe, rating: newRating };
// Update the recipe in localStorage
const storedRecipes = localStorage.getItem('recipes');
if (storedRecipes) {
const recipes = JSON.parse(storedRecipes);
const updatedRecipes = recipes.map((rec) =>
rec.id === id ? updatedRecipe : rec
);
localStorage.setItem('recipes', JSON.stringify(updatedRecipes));
setRecipe(updatedRecipe);
}
}
};
if (loading) {
return <p>Loading...</p>; // Or a "Recipe not found" message
}
if (!recipe) {
return <p>Recipe not found.</p>; // Or a "Recipe not found" message
}
return (
<div className="container">
<h1>{recipe.name}</h1>
<RecipeCard recipe={recipe} onRatingChange={handleRatingChange} />
<div className="recipe-rating">
<RatingStars rating={recipe.rating} onRatingChange={handleRatingChange} />
</div>
</div>
);
}
export default RecipeDetails;
Now, the user will see a “Loading…” message while the recipe details are being fetched.
Deployment
Once you’ve finished building and testing your app, it’s time to deploy it so others can use it. Next.js offers several deployment options, including:
- Vercel: Vercel is the easiest way to deploy Next.js apps. It’s built by the same team that created Next.js, and it offers a seamless deployment experience.
- Netlify: Netlify is another popular platform for deploying web applications. It offers features like continuous deployment and serverless functions.
- Other Platforms: You can also deploy your app to other platforms like AWS, Google Cloud, or Azure.
To deploy to Vercel, follow these steps:
- Create a Vercel Account: If you don’t already have one, sign up for a Vercel account at https://vercel.com/.
- Connect Your Git Repository: Connect your project’s Git repository (e.g., GitHub, GitLab, or Bitbucket) to Vercel.
- Deploy: Vercel will automatically detect your Next.js project and start deploying it.
After the deployment is complete, Vercel will provide you with a unique URL where your app is live.
Common Mistakes and How to Fix Them
Here are some common mistakes beginners make when building Next.js apps and how to fix them:
- Incorrect File Paths: Double-check your file paths, especially when importing components. Next.js can be strict about file organization.
- Missing Dependencies: Make sure you install all the necessary dependencies using npm or yarn.
- Incorrect State Updates: When updating state in React, make sure to create a new object or array instead of directly modifying the existing one. Use the spread operator (
...) to create copies. - Server-Side vs. Client-Side Confusion: Be mindful of where your code is running (server-side or client-side). Some browser-specific APIs won’t work on the server.
- Not Using Dynamic Routes Correctly: When using dynamic routes, make sure to use the
useRouterhook to access the route parameters.
Key Takeaways
- You’ve learned how to set up a Next.js project and structure your application.
- You’ve built reusable components for displaying recipes and handling ratings.
- You’ve implemented dynamic routes to show recipe details.
- You’ve learned how to use
localStorageto store data. - You’ve gained experience with styling your application using CSS.
FAQ
Here are some frequently asked questions about building a recipe rating app with Next.js:
- Can I use a database instead of localStorage? Yes, you can. For larger applications, it’s recommended to use a database (like PostgreSQL, MongoDB, or Firebase) to store your data. You’ll need to set up API routes in Next.js to interact with the database.
- How can I add user authentication? You can use libraries like NextAuth.js or Firebase Authentication to add user authentication to your app.
- How can I improve the performance of my app? You can optimize your app’s performance by using techniques like code splitting, image optimization, and caching.
- Can I add more features to my app? Yes, you can add features like comments, search functionality, and user profiles. The possibilities are endless!
This tutorial has provided a solid foundation for building a recipe rating app with Next.js. By applying the concepts and techniques discussed, you can create a user-friendly and engaging application that allows users to share and discover their favorite recipes. Remember to always test your code thoroughly and experiment with different features to enhance the user experience. Happy coding!
As you continue your journey in web development, remember that practice is key. Building projects like this one is an excellent way to solidify your understanding of Next.js and web development principles. Keep exploring, keep building, and don’t be afraid to experiment with new technologies and features. The skills you’ve gained here will serve as a strong foundation for your future endeavors in creating innovative and interactive web applications, one recipe rating at a time.
