Tue Nov 16 2021
I watched an overview of how a dev presenter at Next.js conf 2021 built a bare bones LMS over a weekend by stitching together existing technologies. I was inspired to try my hand at using Next.js to solve a problem my family has each week: picking out meals for the week and creating a shopping list. I published the resulting code here.
In the spirit of using existing technologies, I explored existing recipe search APIs. I found Edamam recipe API and Spoonacular API. Based on the number of requests I could get away with on a free plan, and the fact that Spoonacular has already built a spiffy meal planner, I chose Edamam.
The Next.js app is hosted by Vercel (the company behind Next.js) and the DB is hosted by MongoDB's managed DB hosting service (Atlas). Both services offer a free tier that I used for this project.
I decided that the user should first see a calendar with links to currently selected meals and buttons to add new meals to a selected date. I briefly evaluated a few existing calendar libraries for React, but none of them really seemed to fit my needs. To be fair, I didn't look very hard. In the end, I decided to just build the calendar from scratch. I hadn't solved the "how do you programmatically make a calendar with JS" problem since school, and never with React, so it was an interesting exercise.
I needed to populate the calendar with currently selected meals, so I exposed an API on my Next.js app (which is incredibly simple to do with Next.js) and used it to query my DB. Originally, I bootstrapped the project with MongoDB's Next.js starter which fetches data using Next's getServerSideProps
function. But, waiting for the server to respond with data before rendering the calendar wasn't quite the super-slick modern web app experience that I was trying to achieve, and relying on getServerSideProps means that the page must refresh or I need to call an API to refresh the data anyway, so I converted it to use SWR to allow the calendar to render while the data is still being fetched. You can see the resulting component here. In the end, it looked like this:
When the user clicks the Add/Edit Meals button on a calendar cell, the browser navigates to a dynamic route that uses the date as a path parameter (e.g. /add-meal/[dateString]). On that page, the user is able to see the currently selected meals, and search for new meals, and select new meals for that date. In this case, I did simply rely on getsServerSideProps to query the DB for existing meal selections before responding.
Edamam provides some decent recipe filtering options out of the box, so I put together a quick and dirty search form that implements most of their existing filters and a keyword search. Then I wired it up to another API on my Next.js app that was responsible for querying Edamam:
The search results are listed below the search page in responsive grid. Pagination is handled with a "Load more results" button:
Finally, I wired the "Add to Plan" button up to the meal plans API that was responsible for populating the calendar so that users can save their selected meals. That was enough for an MVP and my wife and I set about using the app to plan the next week's meals.
After a test run, I consulted with my primary users (my wife and myself) to decide which features would be most helpful. Should I build an ingredients list, a curated list of recipe source filters (I love Food Network recipes, even if they are a bit unnecessarily complex at times), a nutrition analysis, or something else? We both decided that a shopping list generator would be the next most helpful feature. So I set about it.
I added checkboxes on each calendar cell and a "generate shopping list" button beneath the calendar. When a user selects dates with recipes and clicks the button, the shopping list generator presents a list of all possible ingredients (as supplied by Edamam).
The generator shouldn't presume that all ingredients are necessary (we all usually have salt, pepper, and oil, right?), so the user can then click/tap an ingredient to add it to the shopping list. Once the user is satisfied with their selections, they can save the shopping list.
When users have saved shopping lists, they can use the Shopping Lists button on the home page to view a list of saved shopping lists, delete shopping lists, and navigate back to the shopping list generator to edit existing lists. Just like the calendar component, the shopping lists list component uses SWR for data fetching.
After it was all said and done, I probably spent about 10 hours over the course of a few nights working on this project while the wife and baby slept. It's been a handy little tool and saved us some time looking for recipes and parsing ingredients into shopping lists. That is, it did save us time until I found that Edamam doesn't always correctly parse ingredients.
Some recipe results from the Edamam API are missing some if not most ingredients when compared to the actual recipe page that the result references. The Edamam support team has not responded to my request for why or how to avoid this problem, so I've started scoping a project to rebuild the app with Spoonacular... whenever I get around to it. Lesson learned: don't evaluate technology vendors based solely on what their free plan provides (duh). In the meantime, I spend a couple of extra minutes comparing the ingredients in the shopping list generator to the ingredients on the actual recipe.
Otherwise, this was a rewarding project to build a cool tool that my family uses to make our lives easier. I learned to use SWR, I revisited a fun logic problem (procedurally generating a calendar), and my total cost to host the app is $0.00.