In the culinary world, reviews and ratings are the lifeblood of a dish’s popularity. They guide diners, shape restaurant reputations, and even influence the evolution of recipes. Imagine being able to build your own platform, a digital space where users can rate and review their favorite recipes, all powered by the speed and efficiency of Next.js. This tutorial will guide you, step-by-step, through creating such an application: a simple, yet functional, recipe rating system. Whether you’re a beginner or an intermediate developer, this project will provide valuable insights into Next.js, component design, and data handling.
Why Build a Recipe Rating App?
Beyond the fun of building something practical, this project serves multiple learning purposes. It allows you to:
- Master Next.js Fundamentals: You’ll gain hands-on experience with Next.js features like routing, component creation, and state management.
- Understand Frontend Development: Learn how to create interactive user interfaces and handle user input.
- Explore Data Handling: This project will introduce you to basic data management, even if you are not dealing with a full-fledged database.
- Improve Your Problem-Solving Skills: You’ll encounter challenges and learn to troubleshoot and debug your code effectively.
By the end of this tutorial, you’ll have a fully functional recipe rating app and a solid understanding of how to build interactive web applications with Next.js.
Project Setup
Let’s get started. First, we need to set up our Next.js project. Open your terminal and run the following command:
npx create-next-app recipe-rating-app --typescript
This command creates a new Next.js project named `recipe-rating-app` with TypeScript support. Navigate into your project directory:
cd recipe-rating-app
Now, let’s install some dependencies we will need. We will use a library for handling the rating stars, and a library for displaying notifications (optional, but a nice touch).
npm install react-icons react-stars react-toastify
With the project set up and dependencies installed, let’s move on to the next step: designing our components.
Component Design
Our recipe rating app will consist of several components:
- RecipeCard: Displays information about a single recipe, including its name, image, and average rating.
- RecipeList: Renders a list of `RecipeCard` components, displaying all available recipes.
- RatingForm: Allows users to submit ratings and reviews for a specific recipe.
- RecipeDetails: Displays the detailed information about a recipe, including all reviews.
Let’s create these components in the `components` directory. If the directory doesn’t exist, create it in the root of your project.
RecipeCard Component
Create a file named `RecipeCard.tsx` inside the `components` directory. This component will display the recipe’s image, name, and average rating. Here’s the code:
import React from 'react';
import Image from 'next/image';
import ReactStars from 'react-stars';
interface RecipeCardProps {
recipe: {
id: number;
name: string;
image: string;
averageRating: number;
};
}
const RecipeCard: React.FC = ({ recipe }) => {
return (
<div className="recipe-card">
<Image src={recipe.image} alt={recipe.name} width={200} height={150} />
<h3>{recipe.name}</h3>
<div className="rating">
<ReactStars edit={false} value={recipe.averageRating} size={24} color2={'#ffd700'} />
</div>
</div>
);
};
export default RecipeCard;
Explanation:
- We import `Image` from `next/image` for optimized image rendering.
- We import `ReactStars` for the star rating display.
- The `RecipeCardProps` interface defines the structure of the `recipe` prop.
- The component receives a `recipe` object as a prop and displays its details.
RecipeList Component
Create a file named `RecipeList.tsx` in the `components` directory. This component will render a list of `RecipeCard` components. For now, we’ll hardcode some recipe data for demonstration purposes.
import React from 'react';
import RecipeCard from './RecipeCard';
interface RecipeListProps {
recipes: {
id: number;
name: string;
image: string;
averageRating: number;
}[];
}
const RecipeList: React.FC = ({ recipes }) => {
return (
<div className="recipe-list">
{recipes.map((recipe) => (
<RecipeCard key={recipe.id} recipe={recipe} />
))}
</div>
);
};
export default RecipeList;
Explanation:
- The `RecipeList` component receives an array of `recipes` as a prop.
- It maps through the `recipes` array and renders a `RecipeCard` for each recipe.
RatingForm Component
Create a file named `RatingForm.tsx` inside the `components` directory. This component will allow users to submit ratings and reviews. This is a simplified version, as we won’t implement backend functionality in this tutorial. We’ll simply display a form.
import React, { useState } from 'react';
import ReactStars from 'react-stars';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
interface RatingFormProps {
recipeId: number;
onRatingSubmit: (rating: number, review: string) => void;
}
const RatingForm: React.FC = ({ recipeId, onRatingSubmit }) => {
const [rating, setRating] = useState(0);
const [review, setReview] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (rating === 0) {
toast.warn('Please provide a rating!', { position: 'top-right', autoClose: 3000 });
return;
}
onRatingSubmit(rating, review);
setRating(0);
setReview('');
toast.success('Rating submitted!', { position: 'top-right', autoClose: 3000 });
};
return (
<div className="rating-form">
<ToastContainer />
<h4>Rate this recipe:</h4>
<ReactStars
count={5}
onChange={(newRating) => setRating(newRating)}
size={24}
color2={'#ffd700'}
value={rating}
/>
<form onSubmit={handleSubmit}>
<textarea
placeholder="Write a review..."
value={review}
onChange={(e) => setReview(e.target.value)}
/>
<button type="submit">Submit Rating</button>
</form>
</div>
);
};
export default RatingForm;
Explanation:
- We use the `useState` hook to manage the rating and review text.
- The `handleSubmit` function is called when the form is submitted. In a real application, you would send this data to a backend.
- We include basic validation to ensure a rating is provided.
- We use `React-toastify` to display a notification on submission.
RecipeDetails Component
Create a file named `RecipeDetails.tsx` inside the `components` directory. This component will display the detailed information about a recipe and the reviews.
import React, { useState } from 'react';
import ReactStars from 'react-stars';
import RatingForm from './RatingForm';
interface RecipeDetailsProps {
recipe: {
id: number;
name: string;
image: string;
averageRating: number;
reviews: {
rating: number;
reviewText: string;
}[];
};
}
const RecipeDetails: React.FC = ({ recipe }) => {
const [reviews, setReviews] = useState(recipe.reviews);
const handleRatingSubmit = (rating: number, reviewText: string) => {
const newReview = { rating, reviewText };
setReviews((prevReviews) => [...prevReviews, newReview]);
// In a real application, you would also update the average rating and send the data to the backend.
};
return (
<div className="recipe-details">
<img src={recipe.image} alt={recipe.name} width="300" height="200" />
<h2>{recipe.name}</h2>
<div className="rating">
<ReactStars edit={false} value={recipe.averageRating} size={24} color2={'#ffd700'} />
</div>
<RatingForm recipeId={recipe.id} onRatingSubmit={handleRatingSubmit} />
<h3>Reviews:</h3>
<div className="reviews">
{reviews.map((review, index) => (
<div key={index} className="review">
<p>Rating: {review.rating}</p>
<p>{review.reviewText}</p>
</div>
))}
</div>
</div>
);
};
export default RecipeDetails;
Explanation:
- The `RecipeDetails` component receives a `recipe` object as a prop.
- It displays the recipe’s image, name, average rating, and a list of reviews.
- It includes the `RatingForm` component for users to submit ratings.
- It uses the `useState` hook to manage the reviews.
- The `handleRatingSubmit` function updates the reviews.
Styling the Components
To make our app look visually appealing, we need to add some styles. Create a file named `global.css` in the `styles` directory (if you don’t have this directory, create it). Add the following CSS:
/* styles/global.css */
.recipe-card {
border: 1px solid #ccc;
padding: 10px;
margin-bottom: 20px;
border-radius: 8px;
text-align: center;
}
.recipe-card img {
border-radius: 8px;
margin-bottom: 10px;
}
.recipe-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.rating {
margin-bottom: 10px;
}
.rating-form {
margin-top: 20px;
padding: 10px;
border: 1px solid #eee;
border-radius: 8px;
}
.rating-form textarea {
width: 100%;
margin-bottom: 10px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
.rating-form button {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.rating-form button:hover {
background-color: #3e8e41;
}
.recipe-details {
margin-top: 20px;
padding: 20px;
border: 1px solid #eee;
border-radius: 8px;
}
.reviews {
margin-top: 20px;
}
.review {
border: 1px solid #ddd;
padding: 10px;
margin-bottom: 10px;
border-radius: 4px;
}
To apply these styles, import the `global.css` file in your `_app.tsx` file (in the `pages` directory):
// pages/_app.tsx
import '../styles/global.css';
import type { AppProps } from 'next/app';
function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}
export default MyApp;
Building the Pages
Now, let’s create the pages for our application. We’ll have two main pages:
- Index Page (/) : Displays the list of recipes.
- Recipe Details Page (/recipe/[id]) : Displays the details of a specific recipe.
Index Page
Create a file named `pages/index.tsx`. This will be the main page of our app, displaying the recipe list. Add the following code:
// pages/index.tsx
import React from 'react';
import RecipeList from '../components/RecipeList';
const recipes = [
{
id: 1,
name: 'Spaghetti Carbonara',
image: '/spaghetti-carbonara.jpg',
averageRating: 4.5,
},
{
id: 2,
name: 'Chicken Stir-Fry',
image: '/chicken-stir-fry.jpg',
averageRating: 4.0,
},
// Add more recipes here
];
const Home: React.FC = () => {
return (
<div>
<h1>Recipe Ratings</h1>
<RecipeList recipes={recipes} />
</div>
);
};
export default Home;
Explanation:
- We import the `RecipeList` component.
- We create a sample `recipes` array with some hardcoded data.
- We pass the `recipes` data to the `RecipeList` component.
You’ll also need to add images to the `public` directory. Create a directory named `public` in the root of your project and save image files named `spaghetti-carbonara.jpg` and `chicken-stir-fry.jpg` (or use your own images and update the image paths accordingly).
Recipe Details Page
Create a file named `pages/recipe/[id].tsx`. This will be the page that displays the details of a specific recipe. This uses dynamic routing.
// pages/recipe/[id].tsx
import React from 'react';
import { useRouter } from 'next/router';
import RecipeDetails from '../../components/RecipeDetails';
const recipes = [
{
id: 1,
name: 'Spaghetti Carbonara',
image: '/spaghetti-carbonara.jpg',
averageRating: 4.5,
reviews: [
{ rating: 5, reviewText: 'Delicious!' },
{ rating: 4, reviewText: 'Very good.' },
],
},
{
id: 2,
name: 'Chicken Stir-Fry',
image: '/chicken-stir-fry.jpg',
averageRating: 4.0,
reviews: [
{ rating: 4, reviewText: 'Quick and easy.' },
],
},
];
const RecipeDetailPage: React.FC = () => {
const router = useRouter();
const { id } = router.query;
const recipeId = Number(id);
const recipe = recipes.find((recipe) => recipe.id === recipeId);
if (!recipe) {
return <p>Recipe not found</p>;
}
return (
<div>
<h1>{recipe.name}</h1>
<RecipeDetails recipe={recipe} />
</div>
);
};
export default RecipeDetailPage;
Explanation:
- We import `useRouter` from `next/router` to access the route parameters.
- We retrieve the `id` from the route parameters.
- We find the recipe with the matching `id` from our hardcoded `recipes` array.
- We render the `RecipeDetails` component, passing the recipe data as a prop.
Testing and Running the Application
Now that we’ve built the components and pages, let’s run our application. Open your terminal and run the following command:
npm run dev
This command starts the development server. Open your web browser and go to `http://localhost:3000`. You should see the list of recipes. Clicking on a recipe should take you to the recipe details page.
Common Mistakes and How to Fix Them
As you build this application, you might encounter some common mistakes. Here are a few, along with how to fix them:
- Incorrect Image Paths: If your images aren’t displaying, double-check the image paths in your components. Make sure the paths are relative to the `public` directory. For example, if your image is in `public/images/recipe.jpg`, the path should be `/images/recipe.jpg`.
- Missing Dependencies: Ensure you’ve installed all the necessary dependencies, such as `react-stars` and `react-toastify`. Run `npm install` in your project directory to install any missing dependencies.
- Type Errors: TypeScript can help catch errors. Make sure your types are correct and that you’re passing the right props to your components. Use the type definitions in the interfaces to catch these errors.
- Routing Issues: If your routes aren’t working as expected, verify that your file names and directory structures match the Next.js routing conventions. For dynamic routes, make sure you’re using the correct syntax (e.g., `pages/recipe/[id].tsx`).
- CSS Issues: If your styles aren’t being applied, double-check that you’ve imported the `global.css` file correctly in your `_app.tsx` file and that your CSS class names match the ones used in your components.
Key Takeaways
In this tutorial, we’ve walked through building a simple recipe rating app with Next.js. You’ve learned how to set up a Next.js project, design components, handle data (even if it’s hardcoded), and implement basic routing. You’ve also learned about the importance of styling and how to handle user input.
FAQ
Q: How can I store the recipe data persistently?
A: In a real-world application, you would store recipe data in a database (e.g., MongoDB, PostgreSQL) or use a service like Firebase. This tutorial uses hardcoded data for simplicity. You would need to set up a backend API to interact with the database and fetch/store data.
Q: How can I handle user authentication?
A: You can implement user authentication using libraries like NextAuth.js or Firebase Authentication. This involves setting up authentication providers (e.g., email/password, Google, Facebook) and managing user sessions.
Q: How can I improve the app’s performance?
A: Next.js provides many performance optimization features, such as image optimization, code splitting, and server-side rendering. You can also optimize your components by using memoization and lazy loading.
Q: Can I deploy this app?
A: Yes, you can deploy your Next.js app to platforms like Vercel, Netlify, or AWS. Vercel is the easiest option, as it’s specifically designed for Next.js applications.
Q: How can I add more features?
A: You can extend this app by adding features like user accounts, search functionality, filtering and sorting, and more detailed recipe information. You could also integrate a backend for data storage and user authentication.
Building this recipe rating app gives you a solid foundation for understanding Next.js and frontend development. It’s a project that can be expanded and customized to fit your specific needs and interests. The concepts learned here can be applied to a variety of web application projects, making it a valuable learning experience. Remember that the key is to experiment, iterate, and continuously learn.
