Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 75 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,81 @@
# Final Project
# Seasonal Recipes

Replace this readme with your own information about your project.
A full-stack web application for discovering and saving seasonal recipes. Browse recipes by season, filter by diet and allergies, rate recipes, and save your favourites.

Start by briefly describing the assignment in a sentence or two. Keep it short and to the point.
**Live app:** [agnesfinal-project.netlify.app](https://agnesfinal-project.netlify.app/)
**API:** [seasoned-api.onrender.com](https://seasoned-api.onrender.com/)

## The problem
---

Describe how you approached to problem, and what tools and techniques you used to solve it. How did you plan? What technologies did you use? If you had more time, what would be next?
## Features

## View it live
- Browse recipes filtered by season (spring, summer, autumn, winter)
- Filter by diet (vegan, vegetarian) and allergies (lactose-free, gluten-free)
- Search recipes by name
- Sort by most/least popular based on ratings
- Rate recipes
- User registration and login
- Save and manage favourite recipes (requires login)

Every project should be deployed somewhere. Be sure to include the link to the deployed project so that the viewer can click around and see what it's all about.
## Tech Stack

**Frontend**
- React
- React Router
- Axios

**Backend**
- Node.js
- Express
- MongoDB with Mongoose
- bcryptjs (password hashing)

## Getting Started

### Backend

```bash
cd backend
npm install
npm run dev
```

Create a `.env` file in the `backend` folder:

```
MONGO_URL=your_mongodb_connection_string
PORT=8080
```

### Frontend

```bash
cd frontend
npm install
npm run dev
```

The frontend expects the API to run on `http://localhost:8080`. Update `frontend/src/utils/api.js` if needed.

## API Endpoints

| Method | Endpoint | Description | Auth required |
|--------|----------|-------------|---------------|
| GET | `/recipes` | Get all recipes. Query: `?season=spring&search=pasta` | No |
| GET | `/recipes/:id` | Get a single recipe | No |
| GET | `/recipes/popular` | Get recipes sorted by average rating | No |
| GET | `/recipes/:id/reviews` | Get reviews for a recipe | No |
| POST | `/recipes/:id/reviews` | Add a rating to a recipe | No |
| POST | `/users` | Register a new user | No |
| POST | `/sessions` | Login | No |
| GET | `/favourites` | Get user's saved recipes | Yes |
| POST | `/favourites/:recipeId` | Save a recipe | Yes |
| DELETE | `/favourites/:recipeId` | Remove a saved recipe | Yes |

## Deployment

- Frontend deployed on [Netlify](https://agnesfinal-project.netlify.app/)
- Backend deployed on [Render](https://seasoned-api.onrender.com/)
- Database hosted on MongoDB Atlas

> Note: The backend runs on Render's free tier and may take up to 60 seconds to wake up after inactivity.
59 changes: 59 additions & 0 deletions backend/Routes/favouriteRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import express from "express";
import authenticateUser from "../middleware/auth.js";
import Favorite from "../models/favorite.js";

const router = express.Router();

// Get all favourites for the logged-in user
router.get("/favourites", authenticateUser, async (req, res) => {
try {
const favourites = await Favorite.find({ userId: req.user._id }).populate("recipeId");
res.json(favourites);
} catch (err) {
res.status(500).json({ error: "Could not fetch favourites" });
}
});

// Save a recipe as favourite
router.post("/favourites/:recipeId", authenticateUser, async (req, res) => {
try {
const existing = await Favorite.findOne({
userId: req.user._id,
recipeId: req.params.recipeId,
});

if (existing) {
return res.status(409).json({ error: "Recipe already saved" });
}

const favourite = new Favorite({
userId: req.user._id,
recipeId: req.params.recipeId,
});

await favourite.save();
res.status(201).json(favourite);
} catch (err) {
res.status(400).json({ error: "Could not save favourite" });
}
});

// Remove a favourite
router.delete("/favourites/:recipeId", authenticateUser, async (req, res) => {
try {
const deleted = await Favorite.findOneAndDelete({
userId: req.user._id,
recipeId: req.params.recipeId,
});

if (!deleted) {
return res.status(404).json({ error: "Favourite not found" });
}

res.json({ message: "Favourite removed" });
} catch (err) {
res.status(400).json({ error: "Could not remove favourite" });
}
});

export default router;
46 changes: 46 additions & 0 deletions backend/Routes/reviewRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import express from "express";
import Review from "../models/review.js";

const router = express.Router();

// Get popular recipes sorted by average rating
router.get("/recipes/popular", async (req, res) => {
try {
const ratings = await Review.aggregate([
{ $group: { _id: "$recipeId", avgRating: { $avg: "$rating" } } },
{ $sort: { avgRating: -1 } },
]);
res.json(ratings);
} catch (err) {
res.status(500).json({ error: "Could not fetch popular recipes" });
}
});

// Get all reviews for a recipe
router.get("/recipes/:recipeId/reviews", async (req, res) => {
try {
const reviews = await Review.find({
recipeId: req.params.recipeId,
}).populate("userId", "name");
res.json(reviews);
} catch (err) {
res.status(500).json({ error: "Could not fetch reviews" });
}
});

// Add a review
router.post("/recipes/:recipeId/reviews", async (req, res) => {
try {
const review = new Review({
recipeId: req.params.recipeId,
rating: req.body.rating,
});

await review.save();
res.status(201).json(review);
} catch (err) {
res.status(400).json({ error: "Could not create review" });
}
});

export default router;
51 changes: 51 additions & 0 deletions backend/Routes/userRoutes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import express from "express";
import jwt from "jsonwebtoken";
import User from "../models/user.js";
import bcrypt from "bcryptjs";

const router = express.Router();

// Register new user
router.post("/users", async (req, res) => {
try {
// Get data from request body
const { name, email, password } = req.body;

// Does the email already exist?
const existingUser = await User.findOne({ email });
if (existingUser) {
// if the email already exists, return error messages
return res.status(400).json({ error: "That email already exists" });
}

// Create a new user with a hashed password
const user = new User({ name, email, password: bcrypt.hashSync(password) });

await user.save();

// Return user ID and token on successful registration
res.status(201).json({ id: user._id, accessToken: user.accessToken, name: user.name });
} catch (err) {
res
.status(400)
.json({ message: "Could not create user", errors: err.errors });
}
});

// When user Log in, send email and password, receive accessToken.
router.post("/sessions", async (req, res) => {
try {
const user = await User.findOne({ email: req.body.email });

// Check if the user exists AND if the password is correct
if (user && bcrypt.compareSync(req.body.password, user.password)) {
res.json({ userId: user._id, accessToken: user.accessToken, name: user.name });
} else {
res.status(401).json({ message: "Invalid email or password" });
}
} catch (err) {
res.status(500).json({ error: "Login failed" });
}
});

export default router;
Loading