Log your recipes and start meal-planning now!
A full-stack web app that enables users to more effectively plan their meals for the week.
Honeydew is designed to fast-track the meal-planning process. The user starts by logging their favorite recipes, which are then presented in a recipe bank to select and place within their calendar week.
By selecting a recipe and placing it into a mealplan slot (e.g., Monday, dinner), the recipe becomes 'active' so that its ingredients are added to the user's grocery list. Once the user is done placing recipes into their mealplan, they can view a summary of their recipes and required ingredients by clicking the "See grocery list" button.
Lastly, the user can view all their recipes on the Profile screen. Recipes are organized by meal type (i.e., breakfast, lunch, dinner, dessert), along with their photos and expected prep & cook times. The user can click into each recipe card to view more recipe details such as serving size, prep instructions, and list of ingredients.
From a technical perspective, honeydew is a full-stack web application. It uses HTML/CSS/Javascript/jQuery/Bootstrap on the front-end, Express on the back-end, and MongoDB for its database. Honeydew was also designed for both desktop & mobile. On desktop, the week's mealplan is laid out like a standard calendar with the days stretching horizontally. On mobile, the mealplan calendar is inverted, displaying the calendar days vertically instead. Mobile functionality was enabled by Bootstrap 4 plus specific media queries.
Data Model
Honeydew data models include:
- User: Object
- name: String (e.g., 'Graeme')
- email: String (e.g., '[email protected]')
- password: String
- recipes: Array containing Recipe schema
- mealPlan: Array of Strings
- Recipe: Object
- recipeName: String (e.g., Granola)
- servingSize: Number (e.g., 2)
- prepInstructions: String (e.g., Bake in oven at 400 degrees)
- activeCount: Number (e.g., 1)
- ingredients: Array containing Ingredient schema
- Ingredient: Object
- ingredientName: String (e.g., oats)
- qty: String (e.g., 3.5)
- measuringUnit: String (e.g., cups)
Third Party API
Honeydew integrates with Cloudinary for 3rd party image storage, allowing the user to upload their most delicious-looking photos to go along with their recipes. Honeydew both writes to and reads from Cloudinary for image upload & image loading.
Routes
URL | Method | Purpose |
---|---|---|
/ | GET | Render home screen (recipes & mealplan) |
/auth/login | GET | Render login screen |
/auth/login | POST | Accept login credentials |
/auth/signup | GET | Render signup screen |
/auth/signup | POST | Accept signup details |
/api/recipes | GET | Get user's recipes |
/api/recipes | POST | Create new recipe |
/api/recipes | PUT | Update existing recipe (to increment/decrement active count) |
/api/recipes | DELETE | Delete existing recipe |
/grocerylist | GET | Get user's active recipes with related ingredients |
/profile | GET | Get user's entire list of recipes |
/profile/viewRecipe | GET | Get selected recipe to view recipe details |
/profile/viewRecipe | POST | Post selected recipe ID to back-end |
/profile/viewRecipe | DELETE | Delete selected recipe |
Views
View | Purpose |
---|---|
auth/login.ejs | Display login form |
auth/signup.ejs | Display signup form |
grocerylist.ejs | Display summary of active (selected) recipes as well as related ingredients. |
home.ejs | Display homepage - recipe bank, add recipe button, mealplan calendar w/ active meals, and grocery list button. |
layout.ejs | Initialize html. Link to stylesheets and scripts. Contain references to partial views & body of other views. |
partials/alerts.ejs | Flash success or error alerts to user. |
partials/header.ejs | Contain logo and links to login/logout, signup & profile. |
profile.ejs | Display user's recipes, organized by mealtype (i.e., breakfast, lunch, dinner, dessert) and with links to view specific recipe details. |
viewRecipe.ejs | Display specific selected recipe details. |
Selected Code Snippet
The most complicated section of code relates to handling when the user places a recipe into his/her mealplan. The code below tackles the three possible scenarios:
- No new recipe is selected, and the mealplan slot is not blank. Action: Decrement the existing recipe's active count by one, and clear the mealplan slot of the recipe name.
- A new recipe is selected, and the mealplan slot is blank. Action: Increment selected recipe's active count by one, and populate the mealplan slot with the recipe name.
- A new recipe is selected, and the mealplan slot is not blank. Action: Decrement the existing recipe's active count by one, increment the selected recipe's active count by one, and update the mealplan slot with the selected recipe name.
// PUT route to update user's recipe & mealplan
router.put('/', isLoggedIn, (req,res) => {
let scenario = req.body.scenario;
// scenario: no new recipe is selected, and mealplan slot is not blank - UPDATE PREVIOUS RECIPE AND CLEAR MEAL PLAN SLOT
switch (scenario) {
case 'clear-mealplan-slot':
db.User.findById(res.locals.currentUser.id, function(err, user) {
if (err) { console.log("Error finding user in db", err); };
// decrement cleared recipe's active count
for (let i = 0; i < user.recipes.length; i++) {
if (user.recipes[i].recipeName === req.body.selectedMealPlanSlotExistingRecipe) {
user.recipes[i].activeCount > 0 ? user.recipes[i].activeCount -= 1 : user.recipes[i].activeCount = 0;
}
}
// update mealplan slot to a blank value
user.mealPlan.splice(req.body.selectedMealPlanSlotId,1,"");
user.save();
res.render('home');
})
break;
// scenario: new recipe is selected, and mealplan slot is blank - UPDATE SELECTED RECIPE AND POPULATE MEAL PLAN SLOT
case 'populate-mealplan-slot':
db.User.findById(res.locals.currentUser.id, function(err, user) {
if (err) { console.log("Error finding user in db", err); };
// increment selected recipe's active count
for (let i = 0; i < user.recipes.length; i++) {
if (user.recipes[i].recipeName === req.body.selectedRecipeName) {
user.recipes[i].activeCount += 1;
}
}
// update mealplan slot to selected recipe
user.mealPlan.splice(req.body.selectedMealPlanSlotId,1,req.body.selectedRecipeName);
user.save();
res.render('home');
})
break;
// scenario: new recipe is selected, and mealplan slot is not blank - UPDATE PREVIOUS AND SELECTED RECIPE, AND REPLACE MEAL PLAN SLOT
case 'replace-mealplan-slot':
db.User.findById(res.locals.currentUser.id, function(err, user) {
if (err) { console.log("Error finding user in db", err); };
// decrement previous recipe's active count, and increment selected recipe's active count
for (let i = 0; i < user.recipes.length; i++) {
if (user.recipes[i].recipeName === req.body.selectedMealPlanSlotExistingRecipe) {
user.recipes[i].activeCount > 0 ? user.recipes[i].activeCount -= 1 : user.recipes[i].activeCount = 0;
}
if (user.recipes[i].recipeName === req.body.selectedRecipeName) {
user.recipes[i].activeCount += 1;
}
}
// update mealplan slot to selected recipe
user.mealPlan.splice(req.body.selectedMealPlanSlotId,1,req.body.selectedRecipeName);
user.save();
res.render('home');
})
break;
}
})
- HTML
- CSS
- Javascript
- jQuery
- Bootstrap 4: nav, grid, buttons, button groups, popovers, modals
- MongoDB
- User Auth
- Npm modules: bcrypt, body-parser, cloudinary, connect-flash, dotenv, ejs, express, express-ejs-layouts, express-session, mongoose, morgan, multer, passport, passport-local, path
- FontAwesome
- Animate.css
- Roboto Google Font
- Github
- Heroku
- Trello
I would implement the following additional features:
- Drag & drop recipes to mealplan.
- Smarter, more summarized grocery list.
- A more efficient mechanism for adding recipes.