One of the best exercises you can do is yoga because it helps you work on both strength and flexibility while you practice. You don't have to be great at it to get the benefits, but having the correct posture for your poses at every level is important. Also, finding poses that you like and can ease into can lead you all over the place.
That's why we're going to build an app that lets us upload our favorite yoga poses to Cloudinary so we can compare ourselves to the proper form. I know that it sounds cringy to compare yourself to the correct form, but it's a really good way to see where you need to improve quickly.
Set up the tools
We need to get a few tools in place before we start on the code. First up is the local Postgres instance we'll work with. You can download it here. This is a SQL database that's commonly used in production apps so it'll help to have a little experience with it.
Next, you'll need to create a Cloudinary account to store the yoga pose images and use them in your app. You could use something like S3 buckets to host your images, but it's not as straightforward.
Now we're at the fun part. We can finally make the Redwood app and start writing some code. To bootstrap the app, open a terminal and run the following command.
1$ yarn create redwood-app --typescript yoga-pose-recommender
This creates a completely functional, full-stack app with TypeScript and Prisma as its database ORM. You'll find the front-end React code in the web
directory and the back-end GraphQL server in the api
directory. If you start up the app with the following command, you'll be able to see the front-end and back-end of the app running.
1$ yarn redwood dev
Since you've seen that the app is running, all we need to do is add the code for our pose purposes. We'll start by connecting the app to our local Postgres instance.
Connecting to the database
The Redwood app needs the connection string to Postgres so it knows where to send the data we're working with. Look in the root of your project and open the .env
file. This is where environment variables are stored and these typically change across the different environments you deploy your apps to.
Go ahead and uncomment the DATABASE_URL
line and update the value to your connection string. An important thing to note is that you don't have to create the table we're going to use. When we do the database migration, the table will be automatically created. So that string could look like this, where yoga_poses
is the table name.
1DATABASE_URL=postgres://admin:postgres@localhost:5432/yoga_poses
With this value in place, we can turn our attention to the other folders and files in this app.
Updating the database schema
Go to api > db
and open the schema.prisma
file. This is where we define the schema we want to migrate to the database. The first line of code we need to update is the provider
. Change that value from sqlite
to postgresql
. You can see where that environment variable with our connection string is being used here.
Next, go ahead and delete the example model and replace it with this.
1model Pose {2 id Int @id @default(autoincrement())3 name String4 url String5 you_url String6 category String7}
This defines the table schema that the app will use. The URL values will come from Cloudinary uploads and the category will come from a dropdown on the front-end. We're finished with this file so let's actually seed our database with one initial row.
Seeding the database
Go out to the root of the project and look in the scripts
folder. Open the seed.ts
file. This is where we'll define the data for that one row. Upload a yoga pose you like and then a picture of your attempt to Cloudinary. We'll need those URLs for the seed data.
There's a lot to sift through in this boilerplate code, so just delete everything out and paste this in.
1import type { Prisma } from '@prisma/client'2import { db } from 'api/src/lib/db'34export default async () => {5 try {6 const data: Prisma.PoseCreateInput['data'][] = [7 { name: 'tree-pose', url: 'link_to_tree_pose_png', you_url: 'link_to_your_tree_pose_png', category: 'upright'},8 ]910 Promise.all(11 data.map(async (data: Prisma.PoseCreateInput['data']) => {12 const record = await db.pose.create({ data })13 console.log(record)14 })15 )16 } catch (error) {17 console.warn('Please define your seed data.')18 console.error(error)19 }20}
Note that you'll need to update the values for url
and you_url
in the data
array. That's all you'll need to do in order to create the initial record. This data
array is then passed into a promise that executes the create transaction on the database.
This is a good place to run a database migration to get all of the schema changes in place and to seed the table. Redwood has a command that lets us run migrations quickly. Open your terminal and run the following command.
1$ yarn redwood prisma migrate dev
You'll be prompted to enter a name for the migration and then it will run. Check out your local Postgres database to see that seeded data! That's all for the database side of things. Now let's see how fast we can spin up the back-end with Redwood.
Creating the GraphQL server
There are a lot of commands that do some heavy lifting for us. We're going to create the CRUD for our back-end with just one command. Run this in your terminal at the root of the project.
1$ yarn redwood generate sdl --crud pose
Now take a look in api > src > graphql
and you'll see a new sdl
file. This has all of the types for the queries and mutations we need for our GraphQL server. Now head over to api > src > services > poses
and you see several files. The main one is poses.ts
and it has all of the queries and mutations that we need to get started.
While we have a great base to build on, there's still one more mutation we need to add. We need the ability to recommend similar poses based on the category a user selects.
Building recommender query
We need to define the type for this new query so let start there. Go to api > src > graphql
and open the poses.sdl.ts
file. You'll see an object named type Query
and this is where we'll add the following code below the existing queries.
1getPosesByCategory(category: String!): [Pose!]! @requireAuth
This lets the server and the client know that a category
value is expected in the request and an array of pose data can be expected in the response. Now we need to write the actual mutation that retrieves the data and passes it to the front-end.
Open that poses.ts
file we mentioned above. This is where we'll add our category recommender query. You can drop the following code right below the pose
query.
1export const getPosesByCategory = ({ category }) => {2 return db.pose.findMany({3 where: { category },4 })5}
That's all for our extra resolver! Now we can turn our attention to the front-end and build the user interface.
Moving to the client-side
We'll be making a relatively barebones front-end in regards to styles, so if you want to play with your CSS skills this will give you a good project to expand on.
First, let's install the Cloudinary upload widget package to handle image inputs. So open your terminal and go to the web
directory and add these packages with the following command.
1$ yarn add react-cloudinary-upload-widget axios
This is a good time for us to use some of the Redwood functionality we have available. Open your terminal and go to the root of the project and run the following command.
1$ yarn redwood generate page pose
This will create a new page and route for how we upload and display the yoga poses we have to work with. Take a look in web > src > pages > PosePage
. You'll see three files, one of them is for tests and the other is a Storybook document. Then there's the main one we'll be working with, PosePage.tsx
. So open up this file and modify it to look like this.
1import { MetaTags } from '@redwoodjs/web'23const PosePage = () => {4 return (5 <>6 <MetaTags title="Pose" description="Pose page" />7 </>8 )9}1011export default PosePage
We'll add to this component as we build out the functionality. Let's start with the image uploader.
Adding the definitions for the GraphQL server
Now we can get to some of the fun stuff. Let's start by adding a few imports. This will cover all of the imports that we need for the rest of the app.
1import { MetaTags, useMutation, useQuery } from '@redwoodjs/web'2import { useState } from 'react'3import { WidgetLoader, Widget } from 'react-cloudinary-upload-widget'
We need a definition for creating a new pose record so that we can access the GraphQL mutation. We'll add this right below our import statements.
1const CREATE_POSE_MUTATION = gql`2 mutation CreatePoseMutation($input: CreatePoseInput!) {3 createPose(input: $input) {4 name5 }6 }7`
This uses the GraphQL query language so that we can specify what we send to the server and the data we expect back. With some slight modification, you could also run this mutation in the GraphQL playground. We'll also need a definition to query the poses by category. Add this right below the previous definition.
1const GET_POSES_BY_CATEGORY = gql`2 query GetPosesByCategory($category: String!) {3 getPosesByCategory(category: $category) {4 category5 name6 url7 you_url8 }9 }10`
This query will pass the category to the server and then return all of the info for all of the matching pose records. Then we need to add a few variables and methods just inside of the PosePage
component.
1const [category, setCategory] = useState<string>("upright")2const [name, setName] = useState<string>("tree")3const { data, loading } = useQuery(GET_POSES_BY_CATEGORY, {4 variables: { category }5})6const [createPose] = useMutation(CREATE_POSE_MUTATION)
We set a few states to some default values and we use a couple of hooks to get the data from our pose query and to make the method we'll use to upload new pose records. Now we can start adding some stuff to the page that users can interact with.
Uploading new images and creating new records
Let's add the upload widget along with a couple of input fields to the return statement. So your code should look like this.
1return (2 <>3 <MetaTags title="Pose" description="Pose page" />4 <div>5 <label htmlFor="name">Name of the pose:</label>6 <input name="name" type="text" onChange={e => setName(e.currentTarget.value)} />7 </div>8 <div>9 <label htmlFor="category">Category of the pose:</label>10 <select name="category" onChange={e => setCategory(e.currentTarget.value)}>11 <option value="upright">Upright</option>12 <option value="laying">Laying</option>13 <option value="side">Side</option>14 <option value="back">Back</option>15 <option value="arms">Arms</option>16 </select>17 </div>18 <WidgetLoader />19 <Widget20 sources={['local', 'camera']}21 cloudName={`${cloudName}`}22 uploadPreset={`${uploadPreset}`}23 buttonText={'Add Pose Images'}24 multiple={true}25 cropping={false}26 folder={'yoga_poses'}27 style={{28 color: 'white',29 border: 'none',30 width: '120px',31 backgroundColor: 'green',32 borderRadius: '4px',33 height: '25px',34 }}35 onSuccess={uploadImagesFn}36 />37 </>38)
Even though there is a decent amount of code here, nothing too crazy is happening. We have a couple of <div>
elements that hold the name and category inputs. These inputs update their respective state whenever there is a change in their value. The <select>
gives users a set number of categories they can choose from.
The <Widget>
element has some props on it that let us interact with Cloudinary. You'll need to insert your account values for the cloudName
and uploadPreset
values. One interesting thing to note is that in order to upload multiple images, we have to disable the cropping feature. Then the onSuccess
prop has a function we need to make so that when we're done adding our images, it saves the record to the database.
We'll add the uploadImageFn
just below the createPose
method we created earlier.
1function uploadImagesFn(res: CloudinaryResult) {2 const input = {3 category: category,4 name: name,5 url: res.info?.url,6 you_url: res.info?.url7 }89 createPose({ variables: { input } })10}
This function creates an input
variable that collects all of the values we need and then it calls the createPose
method to add the record to the table.
Displaying the images
Now we can finish up by displaying the images on the page by the category a user selects. In the query where we get our data
, there's also the loading
value. This tells us whether the data is still loading from the database or not. We'll use this to render a loading element to prevent the app from crashing when the data isn't available yet. Add this code right after the uploadImagesFn
function.
1if (loading) {2 <div>loading...</div>3}
Now all that's left is rendering the images! Add the following code below the <Widget>
component.
1<div style={{ display: 'block' }}>2 {data?.getPosesByCategory &&3 data.getPosesByCategory.map(image => (4 <>5 <h2>{image.name}</h2>6 <h3>{image.category}</h3>7 <img8 key={`${image.name}_orig`}9 style={{ height: '500px', marginRight: '25px', width: '500px' }}10 src={`${image.url}`}11 />12 <img13 key={image.name}14 style={{ height: '500px', width: '500px' }}15 src={`${image.you_url}`}16 />17 </>18 ))19 }20</div>
This does a quick check to make sure we have data and then it iterates through each pose record and renders the images on the page. When you run your app with yarn redwood dev
, you should see something like this.
That's it! Now whenever you're in the mood to work on some yoga poses, you can check out the correct posture, compare it to your own, and start slowly improving.
Finished code
You can check out the complete code in the yoga-pose-recommender
folder of this repo. You can also check out the front-end in this Code Sandbox.
Conclusion
Sometimes there aren't apps out there that you really like or you just don't want them to have your data. It's ok to make a quick little app that focuses on exactly what you need. That way you get to practice your coding and you get to exercise!