{"id":28044,"date":"2022-03-21T19:25:20","date_gmt":"2022-03-21T19:25:20","guid":{"rendered":"http:\/\/Building-a-Yoga-Pose-App-in-Redwood"},"modified":"2022-03-21T19:25:20","modified_gmt":"2022-03-21T19:25:20","slug":"building-a-yoga-pose-app-in-redwood","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/","title":{"rendered":"Building a Yoga Pose App in Redwood"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p>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\u2019t 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.<\/p>\n<p>That\u2019s why we\u2019re 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\u2019s a really good way to see where you need to improve quickly.<\/p>\n<h2>Set up the tools<\/h2>\n<p>We need to get a few tools in place before we start on the code. First up is the local Postgres instance we\u2019ll work with. You can <a href=\"https:\/\/www.postgresql.org\/download\/\">download it here<\/a>. This is a SQL database that\u2019s commonly used in production apps so it\u2019ll help to have a little experience with it.<\/p>\n<p>Next, you\u2019ll need to <a href=\"https:\/\/cloudinary.com\/users\/register\/free\">create a Cloudinary account<\/a> to store the yoga pose images and use them in your app. You <em>could<\/em> use something like S3 buckets to host your images, but it\u2019s not as straightforward.<\/p>\n<p>Now we\u2019re 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.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn create redwood-app --typescript yoga-pose-recommender\n<\/code><\/span><\/pre>\n<p>This creates a completely functional, full-stack app with TypeScript and <a href=\"https:\/\/www.prisma.io\/\">Prisma<\/a> as its database ORM. You\u2019ll find the front-end React code in the <code>web<\/code> directory and the back-end GraphQL server in the <code>api<\/code> directory. If you start up the app with the following command, you\u2019ll be able to see the front-end and back-end of the app running.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood dev\n<\/code><\/span><\/pre>\n<p>Since you\u2019ve seen that the app is running, all we need to do is add the code for our pose purposes. We\u2019ll start by connecting the app to our local Postgres instance.<\/p>\n<h2>Connecting to the database<\/h2>\n<p>The Redwood app needs the connection string to Postgres so it knows where to send the data we\u2019re working with. Look in the root of your project and open the <code>.env<\/code> file. This is where environment variables are stored and these typically change across the different environments you deploy your apps to.<\/p>\n<p>Go ahead and uncomment the <code>DATABASE_URL<\/code> line and update the value to your connection string. An important thing to note is that you don\u2019t have to create the table we\u2019re going to use. When we do the database migration, the table will be automatically created. So that string could look like this, where <code>yoga_poses<\/code> is the table name.<\/p>\n<pre class=\"js-syntax-highlighted\"><code>DATABASE_URL=postgres:\/\/admin:postgres@localhost:5432\/yoga_poses\n<\/code><\/pre>\n<p>With this value in place, we can turn our attention to the other folders and files in this app.<\/p>\n<h3>Updating the database schema<\/h3>\n<p>Go to <code>api &gt; db<\/code> and open the <code>schema.prisma<\/code> 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 <code>provider<\/code>. Change that value from <code>sqlite<\/code> to <code>postgresql<\/code>. You can see where that environment variable with our connection string is being used here.<\/p>\n<p>Next, go ahead and delete the example model and replace it with this.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">model Pose {\n  id        Int     @id @<span class=\"hljs-keyword\">default<\/span>(autoincrement())\n  name      <span class=\"hljs-built_in\">String<\/span>\n  url       <span class=\"hljs-built_in\">String<\/span>\n  you_url   <span class=\"hljs-built_in\">String<\/span>\n  category  <span class=\"hljs-built_in\">String<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>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\u2019re finished with this file so let\u2019s actually seed our database with one initial row.<\/p>\n<h3>Seeding the database<\/h3>\n<p>Go out to the root of the project and look in the <code>scripts<\/code> folder. Open the <code>seed.ts<\/code> file. This is where we\u2019ll define the data for that one row. Upload a yoga pose you like and then a picture of your attempt to Cloudinary. We\u2019ll need those URLs for the seed data.<\/p>\n<p>There\u2019s a lot to sift through in this boilerplate code, so just delete everything out and paste this in.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> type { Prisma } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@prisma\/client'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { db } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'api\/src\/lib\/db'<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> <span class=\"hljs-keyword\">async<\/span> () =&gt; {\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> data: Prisma.PoseCreateInput&#91;<span class=\"hljs-string\">'data'<\/span>]&#91;] = &#91;\n      { <span class=\"hljs-attr\">name<\/span>: <span class=\"hljs-string\">'tree-pose'<\/span>, <span class=\"hljs-attr\">url<\/span>: <span class=\"hljs-string\">'link_to_tree_pose_png'<\/span>, <span class=\"hljs-attr\">you_url<\/span>: <span class=\"hljs-string\">'link_to_your_tree_pose_png'<\/span>, <span class=\"hljs-attr\">category<\/span>: <span class=\"hljs-string\">'upright'<\/span>},\n    ]\n\n    <span class=\"hljs-built_in\">Promise<\/span>.all(\n      data.map(<span class=\"hljs-keyword\">async<\/span> (data: Prisma.PoseCreateInput&#91;<span class=\"hljs-string\">'data'<\/span>]) =&gt; {\n        <span class=\"hljs-keyword\">const<\/span> record = <span class=\"hljs-keyword\">await<\/span> db.pose.create({ data })\n        <span class=\"hljs-built_in\">console<\/span>.log(record)\n      })\n    )\n  } <span class=\"hljs-keyword\">catch<\/span> (error) {\n    <span class=\"hljs-built_in\">console<\/span>.warn(<span class=\"hljs-string\">'Please define your seed data.'<\/span>)\n    <span class=\"hljs-built_in\">console<\/span>.error(error)\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Note that you\u2019ll need to update the values for <code>url<\/code> and <code>you_url<\/code> in the <code>data<\/code> array. That\u2019s all you\u2019ll need to do in order to create the initial record. This <code>data<\/code> array is then passed into a promise that executes the create transaction on the database.<\/p>\n<p>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.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood prisma migrate dev\n<\/code><\/span><\/pre>\n<p>You\u2019ll 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\u2019s all for the database side of things. Now let\u2019s see how fast we can spin up the back-end with Redwood.<\/p>\n<h2>Creating the GraphQL server<\/h2>\n<p>There are a lot of commands that do some heavy lifting for us. We\u2019re going to create the CRUD for our back-end with just one command. Run this in your terminal at the root of the project.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate sdl --crud pose\n<\/code><\/span><\/pre>\n<p>Now take a look in <code>api &gt; src &gt; graphql<\/code> and you\u2019ll see a new <code>sdl<\/code> file. This has all of the types for the queries and mutations we need for our GraphQL server. Now head over to <code>api &gt; src &gt; services &gt; poses<\/code> and you see several files. The main one is <code>poses.ts<\/code> and it has all of the queries and mutations that we need to get started.<\/p>\n<p>While we have a great base to build on, there\u2019s still one more mutation we need to add. We need the ability to recommend similar poses based on the category a user selects.<\/p>\n<h3>Building recommender query<\/h3>\n<p>We need to define the type for this new query so let start there. Go to <code>api &gt; src &gt; graphql<\/code> and open the <code>poses.sdl.ts<\/code> file. You\u2019ll see an object named <code>type Query<\/code> and this is where we\u2019ll add the following code below the existing queries.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css shcb-wrap-lines\"><span class=\"hljs-selector-tag\">getPosesByCategory<\/span>(<span class=\"hljs-selector-tag\">category<\/span>: <span class=\"hljs-selector-tag\">String<\/span>!): <span class=\"hljs-selector-attr\">&#91;Pose!]<\/span>! <span class=\"hljs-keyword\">@requireAuth<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This lets the server and the client know that a <code>category<\/code> 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.<\/p>\n<p>Open that <code>poses.ts<\/code> file we mentioned above. This is where we\u2019ll add our category recommender query. You can drop the following code right below the <code>pose<\/code> query.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> getPosesByCategory = <span class=\"hljs-function\">(<span class=\"hljs-params\">{ category }<\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> db.pose.findMany({\n    <span class=\"hljs-attr\">where<\/span>: { category },\n  })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>That\u2019s all for our extra resolver! Now we can turn our attention to the front-end and build the user interface.<\/p>\n<h2>Moving to the client-side<\/h2>\n<p>We\u2019ll 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.<\/p>\n<p>First, let\u2019s install the Cloudinary upload widget package to handle image inputs. So open your terminal and go to the <code>web<\/code> directory and add these packages with the following command.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn add react-cloudinary-upload-widget axios\n<\/code><\/span><\/pre>\n<p>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.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">$ yarn redwood generate page pose\n<\/code><\/span><\/pre>\n<p>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 <code>web &gt; src &gt; pages &gt; PosePage<\/code>. You\u2019ll see three files, one of them is for tests and the other is a Storybook document. Then there\u2019s the main one we\u2019ll be working with, <code>PosePage.tsx<\/code>. So open up this file and modify it to look like this.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { MetaTags } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> PosePage = <span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">return<\/span> (\n    <span class=\"xml\"><span class=\"hljs-tag\">&lt;&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">MetaTags<\/span> <span class=\"hljs-attr\">title<\/span>=<span class=\"hljs-string\">\"Pose\"<\/span> <span class=\"hljs-attr\">description<\/span>=<span class=\"hljs-string\">\"Pose page\"<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n  )\n}\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">default<\/span> PosePage\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We\u2019ll add to this component as we build out the functionality. Let\u2019s start with the image uploader.<\/p>\n<h3>Adding the definitions for the GraphQL server<\/h3>\n<p>Now we can get to some of the fun stuff. Let\u2019s start by adding a few imports. This will cover all of the imports that we need for the rest of the app.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> { MetaTags, useMutation, useQuery } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'@redwoodjs\/web'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { useState } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react'<\/span>\n<span class=\"hljs-keyword\">import<\/span> { WidgetLoader, Widget } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'react-cloudinary-upload-widget'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>We need a definition for creating a new pose record so that we can access the GraphQL mutation. We\u2019ll add this right below our import statements.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> CREATE_POSE_MUTATION = gql`\n  mutation CreatePoseMutation($input: CreatePoseInput!) {\n    createPose(input: $input) {\n      name\n    }\n  }\n`\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>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\u2019ll also need a definition to query the poses by category. Add this right below the previous definition.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> GET_POSES_BY_CATEGORY = gql`\n  query GetPosesByCategory($category: String!) {\n    getPosesByCategory(category: $category) {\n      category\n      name\n      url\n      you_url\n    }\n  }\n`\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>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 <code>PosePage<\/code> component.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">const<\/span> &#91;category, setCategory] = useState&lt;string&gt;(<span class=\"hljs-string\">\"upright\"<\/span>)\n<span class=\"hljs-keyword\">const<\/span> &#91;name, setName] = useState&lt;string&gt;(<span class=\"hljs-string\">\"tree\"<\/span>)\n<span class=\"hljs-keyword\">const<\/span> { data, loading } = useQuery(GET_POSES_BY_CATEGORY, {\n  <span class=\"hljs-attr\">variables<\/span>: { category }\n})\n<span class=\"hljs-keyword\">const<\/span> &#91;createPose] = useMutation(CREATE_POSE_MUTATION)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>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\u2019ll use to upload new pose records. Now we can start adding some stuff to the page that users can interact with.<\/p>\n<h3>Uploading new images and creating new records<\/h3>\n<p>Let\u2019s add the upload widget along with a couple of input fields to the return statement. So your code should look like this.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-keyword\">return<\/span> (\n  <span class=\"xml\"><span class=\"hljs-tag\">&lt;&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">MetaTags<\/span> <span class=\"hljs-attr\">title<\/span>=<span class=\"hljs-string\">\"Pose\"<\/span> <span class=\"hljs-attr\">description<\/span>=<span class=\"hljs-string\">\"Pose page\"<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">htmlFor<\/span>=<span class=\"hljs-string\">\"name\"<\/span>&gt;<\/span>Name of the pose:<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"name\"<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"text\"<\/span> <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{e<\/span> =&gt;<\/span> setName(e.currentTarget.value)} \/&gt;\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">htmlFor<\/span>=<span class=\"hljs-string\">\"category\"<\/span>&gt;<\/span>Category of the pose:<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">select<\/span> <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"category\"<\/span> <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{e<\/span> =&gt;<\/span> setCategory(e.currentTarget.value)}&gt;\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"upright\"<\/span>&gt;<\/span>Upright<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"laying\"<\/span>&gt;<\/span>Laying<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"side\"<\/span>&gt;<\/span>Side<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"back\"<\/span>&gt;<\/span>Back<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">option<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"arms\"<\/span>&gt;<\/span>Arms<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">option<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">select<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">WidgetLoader<\/span> \/&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Widget<\/span>\n      <span class=\"hljs-attr\">sources<\/span>=<span class=\"hljs-string\">{&#91;<\/span>'<span class=\"hljs-attr\">local<\/span>', '<span class=\"hljs-attr\">camera<\/span>']}\n      <span class=\"hljs-attr\">cloudName<\/span>=<span class=\"hljs-string\">{<\/span>`${<span class=\"hljs-attr\">cloudName<\/span>}`}\n      <span class=\"hljs-attr\">uploadPreset<\/span>=<span class=\"hljs-string\">{<\/span>`${<span class=\"hljs-attr\">uploadPreset<\/span>}`}\n      <span class=\"hljs-attr\">buttonText<\/span>=<span class=\"hljs-string\">{<\/span>'<span class=\"hljs-attr\">Add<\/span> <span class=\"hljs-attr\">Pose<\/span> <span class=\"hljs-attr\">Images<\/span>'}\n      <span class=\"hljs-attr\">multiple<\/span>=<span class=\"hljs-string\">{true}<\/span>\n      <span class=\"hljs-attr\">cropping<\/span>=<span class=\"hljs-string\">{false}<\/span>\n      <span class=\"hljs-attr\">folder<\/span>=<span class=\"hljs-string\">{<\/span>'<span class=\"hljs-attr\">yoga_poses<\/span>'}\n      <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span>\n        <span class=\"hljs-attr\">color:<\/span> '<span class=\"hljs-attr\">white<\/span>',\n        <span class=\"hljs-attr\">border:<\/span> '<span class=\"hljs-attr\">none<\/span>',\n        <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">120px<\/span>',\n        <span class=\"hljs-attr\">backgroundColor:<\/span> '<span class=\"hljs-attr\">green<\/span>',\n        <span class=\"hljs-attr\">borderRadius:<\/span> '<span class=\"hljs-attr\">4px<\/span>',\n        <span class=\"hljs-attr\">height:<\/span> '<span class=\"hljs-attr\">25px<\/span>',\n      }}\n      <span class=\"hljs-attr\">onSuccess<\/span>=<span class=\"hljs-string\">{uploadImagesFn}<\/span>\n    \/&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/&gt;<\/span><\/span>\n)\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Even though there is a decent amount of code here, nothing too crazy is happening. We have a couple of <code>&lt;div&gt;<\/code> elements that hold the name and category inputs. These inputs update their respective state whenever there is a change in their value. The <code>&lt;select&gt;<\/code> gives users a set number of categories they can choose from.<\/p>\n<p>The <code>&lt;Widget&gt;<\/code> element has some props on it that let us interact with Cloudinary. You\u2019ll need to insert your account values for the <code>cloudName<\/code> and <code>uploadPreset<\/code> values. One interesting thing to note is that in order to upload multiple images, we have to disable the cropping feature. Then the <code>onSuccess<\/code> prop has a function we need to make so that when we\u2019re done adding our images, it saves the record to the database.<\/p>\n<p>We\u2019ll add the <code>uploadImageFn<\/code> just below the <code>createPose<\/code> method we created earlier.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">uploadImagesFn<\/span>(<span class=\"hljs-params\">res: CloudinaryResult<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> input = {\n    <span class=\"hljs-attr\">category<\/span>: category,\n    <span class=\"hljs-attr\">name<\/span>: name,\n    <span class=\"hljs-attr\">url<\/span>: res.info?.url,\n    <span class=\"hljs-attr\">you_url<\/span>: res.info?.url\n  }\n\n  createPose({ <span class=\"hljs-attr\">variables<\/span>: { input } })\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This function creates an <code>input<\/code> variable that collects all of the values we need and then it calls the <code>createPose<\/code> method to add the record to the table.<\/p>\n<h3>Displaying the images<\/h3>\n<p>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 <code>data<\/code>, there\u2019s also the <code>loading<\/code> value. This tells us whether the data is still loading from the database or not. We\u2019ll use this to render a loading element to prevent the app from crashing when the data isn\u2019t available yet. Add this code right after the <code>uploadImagesFn<\/code> function.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\">if (loading) {\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>loading...<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>Now all that\u2019s left is rendering the images! Add the following code below the <code>&lt;Widget&gt;<\/code> component.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml shcb-wrap-lines\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">display:<\/span> '<span class=\"hljs-attr\">block<\/span>' }}&gt;<\/span>\n  {data?.getPosesByCategory &amp;&amp;\n    data.getPosesByCategory.map(image =&gt; (\n      <span class=\"hljs-tag\">&lt;&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h2<\/span>&gt;<\/span>{image.name}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h2<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span>&gt;<\/span>{image.category}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n          <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{<\/span>`${<span class=\"hljs-attr\">image.name<\/span>}<span class=\"hljs-attr\">_orig<\/span>`}\n          <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">height:<\/span> '<span class=\"hljs-attr\">500px<\/span>', <span class=\"hljs-attr\">marginRight:<\/span> '<span class=\"hljs-attr\">25px<\/span>', <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">500px<\/span>' }}\n          <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{<\/span>`${<span class=\"hljs-attr\">image.url<\/span>}`}\n        \/&gt;<\/span>\n        <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span>\n          <span class=\"hljs-attr\">key<\/span>=<span class=\"hljs-string\">{image.name}<\/span>\n          <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">{{<\/span> <span class=\"hljs-attr\">height:<\/span> '<span class=\"hljs-attr\">500px<\/span>', <span class=\"hljs-attr\">width:<\/span> '<span class=\"hljs-attr\">500px<\/span>' }}\n          <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">{<\/span>`${<span class=\"hljs-attr\">image.you_url<\/span>}`}\n        \/&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;\/&gt;<\/span>\n    ))\n  }\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>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 <code>yarn redwood dev<\/code>, you should see something like this.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/res.cloudinary.com\/jesse-thisdot\/image\/upload\/c_limit,w_2000\/f_auto\/q_auto\/v1644350194\/e-603fc55d218a650069f5228b\/upq1jotkc5nudkywksmz.png\" alt=\"upload widget, name and category inputs, and image displayed\" loading=\"lazy\" class=\"c-transformed-asset\"  width=\"2000\" height=\"1202\"\/><\/p>\n<p>That\u2019s it! Now whenever you\u2019re 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.<\/p>\n<h2>Finished code<\/h2>\n<p>You can check out the complete code in <a href=\"https:\/\/github.com\/flippedcoder\/media-projects\/tree\/main\/yoga-pose-recommender\">the <code>yoga-pose-recommender<\/code> folder of this repo<\/a>. You can also check out the front-end in <a href=\"https:\/\/codesandbox.io\/s\/musing-jang-y8u53\">this Code Sandbox<\/a>.<\/p>\n<\/div>\n  \n  <div class=\"wp-block-cloudinary-code-sandbox \">\n    <iframe\n      src=\"https:\/\/codesandbox.io\/embed\/musing-jang-y8u53?theme=dark&amp;codemirror=1&amp;highlights=&amp;editorsize=50&amp;fontsize=14&amp;expanddevtools=0&amp;hidedevtools=0&amp;eslint=0&amp;forcerefresh=0&amp;hidenavigation=0&amp;initialpath=%2F&amp;module=&amp;moduleview=0&amp;previewwindow=&amp;view=&amp;runonclick=1\"\n      height=\"500\"\n      style=\"width: 100%;\"\n      title=\"musing-jang-y8u53\"\n      loading=\"lazy\"\n      allow=\"accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking\"\n      sandbox=\"allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts\"\n    ><\/iframe>\n  <\/div>\n\n  <div class=\"wp-block-cloudinary-markdown \"><h2>Conclusion<\/h2>\n<p>Sometimes there aren\u2019t apps out there that you really like or you just don\u2019t want them to have your data. It\u2019s ok to make a quick little app that focuses on exactly what you need. That way you get to practice your coding <em>and<\/em> you get to exercise!<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":41,"featured_media":28045,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[134,370,246,371],"class_list":["post-28044","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-guest-post","tag-image","tag-react","tag-under-review"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Building a Yoga Pose App in Redwood<\/title>\n<meta name=\"description\" content=\"Sometimes you want to mix fitness progress with coding. While you might not run out and buy a Peloton, you might want to improve on some home exercises. Why not build an app to help you improve on the form of your yoga poses?\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Yoga Pose App in Redwood\" \/>\n<meta property=\"og:description\" content=\"Sometimes you want to mix fitness progress with coding. While you might not run out and buy a Peloton, you might want to improve on some home exercises. Why not build an app to help you improve on the form of your yoga poses?\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2022-03-21T19:25:20+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"6192\" \/>\n\t<meta property=\"og:image:height\" content=\"4128\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Building a Yoga Pose App in Redwood\",\"datePublished\":\"2022-03-21T19:25:20+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\"},\"wordCount\":7,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA\",\"keywords\":[\"Guest Post\",\"Image\",\"React\",\"Under Review\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2022\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\",\"url\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\",\"name\":\"Building a Yoga Pose App in Redwood\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA\",\"datePublished\":\"2022-03-21T19:25:20+00:00\",\"description\":\"Sometimes you want to mix fitness progress with coding. While you might not run out and buy a Peloton, you might want to improve on some home exercises. Why not build an app to help you improve on the form of your yoga poses?\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA\",\"width\":6192,\"height\":4128},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Yoga Pose App in Redwood\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"\"}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Building a Yoga Pose App in Redwood","description":"Sometimes you want to mix fitness progress with coding. While you might not run out and buy a Peloton, you might want to improve on some home exercises. Why not build an app to help you improve on the form of your yoga poses?","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/","og_locale":"en_US","og_type":"article","og_title":"Building a Yoga Pose App in Redwood","og_description":"Sometimes you want to mix fitness progress with coding. While you might not run out and buy a Peloton, you might want to improve on some home exercises. Why not build an app to help you improve on the form of your yoga poses?","og_url":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/","og_site_name":"Cloudinary Blog","article_published_time":"2022-03-21T19:25:20+00:00","og_image":[{"width":6192,"height":4128,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA","type":"image\/jpeg"}],"twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/"},"author":{"name":"","@id":""},"headline":"Building a Yoga Pose App in Redwood","datePublished":"2022-03-21T19:25:20+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/"},"wordCount":7,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA","keywords":["Guest Post","Image","React","Under Review"],"inLanguage":"en-US","copyrightYear":"2022","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/","url":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/","name":"Building a Yoga Pose App in Redwood","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA","datePublished":"2022-03-21T19:25:20+00:00","description":"Sometimes you want to mix fitness progress with coding. While you might not run out and buy a Peloton, you might want to improve on some home exercises. Why not build an app to help you improve on the form of your yoga poses?","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA","width":6192,"height":4128},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/guest_post\/building-a-yoga-pose-app-in-redwood\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Building a Yoga Pose App in Redwood"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":""}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1681925574\/Web_Assets\/blog\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e\/357e52054a3ecf26b7abf85b1e5ee4cbaafcef8a-6192x4128-1_280454460e.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28044","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/41"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=28044"}],"version-history":[{"count":0,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/28044\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/28045"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=28044"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=28044"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=28044"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}