Making a Meal Picker

Milecia

Making a Meal Picker with Redwood

Intro

Deciding what to eat every day is something that a lot of us struggle with. It'd be so much easier if there was a button you could push and it would choose one of your favorite meals for you.

That's why we're going to make a meal picker in this tutorial. We'll use Redwood to create the front-end and back-end of the app which will let us save recipes and choose a random meal. We'll also be using Cloudinary to host the video we have associated with the recipes.

Setting up the app

To get started, let's make a new Redwood app that uses TypeScript. In a terminal, run this command.

1yarn create redwood-app --typescript meal-picker

This will generate a lot of new files and folders in the meal-picker directory. The main two directories are web and api. The web folder contains all of the code for the front-end and the api directory contains all of the code for the back-end.

Most times, it's a good practice to start building the data model of a new app first.

Building the back-end

We'll start by opening the prisma.schema file in api > db directory. This file holds all of the models for the tables we'll have in the database. We'll be using a local Postgres instance, so if you need to download that you can find the right version here.

The first thing we'll do is update the provider value to postgresql. Next, we need to create a .env file in the root of the project. You'll see the DATABASE_URL being read from the environment below the type of database we're using. In the .env file, add the connection string to your local Postgres instance. It might look similar to this.

1DATABASE_URL=postgres://postgres:admin@localhost:5432/meal_picker

Creating the model

Now you have everything set to connect to your database. We need to define the model for the meals we'll be saving to choose from. You can delete the UserExample model and replace it with this.

1model Meal {
2 id String @id @default(cuid())
3 title String
4 recipe String
5 video String
6}

This model defines a table that will hold all of the details for our meals. There has to be a way for users to input their favorite meals and all of the details. With the model ready, we can go ahead and run the migration to get these changes on the database.

1yarn rw prisma migrate dev

That will define the Meal table in Postgres. So now we can use one of the Redwood commands to generate this CRUD functionality for us.

Creating the meal collection management functionality

We'll need to have both the GraphQL back-end in place and the front-end in place to allow users to add, edit, and delete meals. This normally takes a bit of time, but the following command generates all of that for us.

1yarn rw g scaffold meal

Check the api > src > graphql directory and you should see meals.sdl.ts file that has all of the types for the queries and mutations we need. If you check in api > src > services > meals, you'll see three files. Two of them are for tests and the meals.ts file holds all of the resolvers for our queries and mutations. These already connect to the database so we have our entire back-end created!

Moving to the front-end

Now that we have the GraphQL server ready to go, let's take a look at the files the scaffold command created on the front-end. In web > src > pages > Meals, you'll see several new directories that correspond to different views of the CRUD for meals.

If you take a look in web > src > components > Meal, you'll see a number of components that we created. These components interact with and display the data that we get from the back-end. It's worth taking the time to peek at these different files and see how they work, but you can still run the app and see all of the CRUD in action without ever looking at the code.

In your terminal, run this command and navigate to http://localhost:8910/meals.

1yarn rw dev

You should see something like this in your browser.

Now add a few entries by clicking the "New Meal" button. This will bring up a new page and let you add the details.

If you haven't uploaded any videos for your meals, take a second to go to your Cloudinary account and add those. Make sure you grab the URLs for the videos you want to use because you'll add them to your new meal entries.

Once you've added a few meals, you should see a table that lists all of your entries.

That's everything we need to handle the CRUD functionality for our meals. You might think of this as an admin area in the app. Redwood generated all of this for us with just one command. All that's left is making the random meal picker page.

Making the picker page

Let's make a new page for the picker. We'll use another Redwood command.

1yarn rw g page picker /

This updates our routes to make the picker page the root page and it generates some files for us. Go to web > src > pages > PickerPage and open PickerPage.tsx. This is where we'll make the button that will tell us what to eat.

We'll get rid of a lot of the boilerplate code in the PickerPage component. Let's start by adding the import statements for the methods we'll be using. So your list of import statements should look like this.

1import { useQuery } from '@redwoodjs/web'
2import { MetaTags } from '@redwoodjs/web'
3import { useState } from 'react'

Now let's add the GraphQL query we need to get all of the meals we have available to choose from. This goes right below the import statements.

1const GET_MEALS = gql`
2 query {
3 meals {
4 title
5 recipe
6 video
7 }
8 }
9`

One more quick thing before we start using this query. Since this is a TypeScript app, let's add the type for a single meal. Below the query just wrote, add the Meal type.

1interface Meal {
2 title: string
3 recipe: string
4 video: string
5}

When we get ready to work with the meals data, now we know exactly what to expect. Now we get to delete a lot of code. Inside the PickerPage component, delete everything except the <MetaTags> element. Your PickerPage.tsx should look like this now.

1import { useQuery } from '@redwoodjs/web'
2import { MetaTags } from '@redwoodjs/web'
3import { useState } from 'react'
4
5const GET_MEALS = gql`
6 query {
7 meals {
8 title
9 recipe
10 video
11 }
12 }
13`
14
15interface Meal {
16 title: string
17 recipe: string
18 video: string
19}
20
21const PickerPage = () => {
22 return (
23 <>
24 <MetaTags
25 title="Picker"
26 />
27 </>
28 )
29}
30
31export default PickerPage

All that's left is adding the data and the elements to display it.

Handling the data

Let's add a new meal state in the component. We'll use our Meal type to define what values are expected.

1const [meal, setMeal] = useState<Meal>()

Next we'll use the useQuery hook to fetch our data from the GraphQL server.

1const { loading, data } = useQuery(GET_MEALS)

We get both the data and a loading state for it. That way we can account for any latency in the request and show the users a loading screen. If we don't handle this, the app will likely crash because the data isn't available yet. Right below the useQuery call, we'll add the code to handle this loading state.

1if (loading) {
2 return <div>Loading...</div>
3}

The last function we need to add before creating the elements to show our random meal will actually be responsible for choosing that meal. When we click a button on the screen, it'll call this function and set the meal state to some random selection.

1const loadMeal = () => {
2 if (data.meals.length !== 0) {
3 const max = data.meals.length
4 const index = getRandomInt(0, max)
5 setMeal(data.meals[index])
6 }
7}

There is a tiny helper function we for to get that random integer. Add this code below the PickerPage component.

1function getRandomInt(min, max) {
2 min = Math.ceil(min);
3 max = Math.floor(max);
4 return Math.floor(Math.random() * (max - min) + min);
5}

We have the data in place and all of the accompanying functions we need. Let's finally add the elements to display everything.

The button and meal display

Below the <MetaTags> element, add these elements.

1<h1>{meal ? meal.title : 'Find out what you are going to eat'}</h1>
2<button onClick={loadMeal} style={{ fontSize: '18px', padding: '24px 32px', width: '500px' }}>Tell me what to eat</button>

The text on the page will change based on whether or not you've had a random meal selected. Then there's the button with a few styles on it that will call the function to choose a new random meal.

If you run the app again with yarn rw dev, you'll see something like this in your browser.

The last piece of code is to display the info for the randomly selected meal. We'll do this with a conditional render statement below the <button>.

1{meal &&
2 <>
3 <p>{meal.recipe}</p>
4 <video src={meal.video} controls height='350' width='500'></video>
5 </>
6}

This will display the recipe and the video whenever the button is clicked and a meal is selected. Now if you look in the browser, you should see something like this.

That's it! You now have a meal picker that will make it hard for you to say you can't decide what to eat anymore.

Finished code

If you want to check out the finished front-end and back-end code, check out the code in the food-picker folder of this repo. You can see an example of the front-end in this Code Sandbox.

Conclusion

Not every project you work on has to be super detailed. Sometimes you just need something to prove a concept or you want to make something for yourself. I know I've definitely used this app to pick what I'm going to eat more than I want to admit.

Milecia

Software Team Lead

Milecia is a senior software engineer, international tech speaker, and mad scientist that works with hardware and software. She will try to make anything with JavaScript first.