Create a Recipe App with Tailwind and Nuxt.js

Demola Malomo - Jan 18 '23 - - Dev Community

According to Verified Market Research, the recipe apps market size is projected to reach USD 1,098.22 Million by 2028. They are designed with intent and excellent user experience to make cooking easier, faster, and more accessible.

In this post, we will learn how to create a recipe app in Nuxt.js using TailwindCSS and Appwrite. The project’s GitHub repository can be found here.

Prerequisites

To fully grasp the concepts presented in this tutorial, the following requirements apply:

  • Basic understanding of JavaScript and Vue.js
  • Docker installation
  • An Appwrite instance; check out this article on how to set up an instance

Getting Started

We need to create a Nuxt.js starter project by navigating to the desired directory and running the command below in our terminal.

npx nuxi init recipe-app && cd recipe-app
Enter fullscreen mode Exit fullscreen mode

The command creates a Nuxt.js project called recipe-app and navigates into the project directory.

Next, we need to install Nuxt.js dependencies by running the command below in our terminal.

npm install
Enter fullscreen mode Exit fullscreen mode

Installing dependencies

Installing TailwindCSS
TailwindCSS is a utility-first CSS framework packed with classes to help us style our web page. To use it in our application, run the command below in our terminal.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
Enter fullscreen mode Exit fullscreen mode

The command installs TailwindCSS and its dependencies and generates a tailwind.config.js file.

Next, we need to update the tailwind.config.js file with the snippet below:

/** @type {import('tailwindcss').Config} */
module.exports = {
    content: [
        './components/**/*.{js,vue,ts}',
        './layouts/**/*.vue',
        './pages/**/*.vue',
        './**/*.vue',
        './plugins/**/*.{js,ts}',
    ],
    theme: {
        extend: {},
    },
    plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Next, we need to add TailwindCSS directives to our application. The directives give our application access to TailwindCSS utility classes. To do this, create a css/tailwind.css file in the root directory and add the snippet below:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to add TailwindCSS as a dependency in the nuxt.config.js, as shown below:

// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
    postcss: {
        plugins: {
            tailwindcss: {},
            autoprefixer: {},
        },
    },
    css: ['~/css/tailwind.css'],
});
Enter fullscreen mode Exit fullscreen mode

Installing Appwrite
Appwrite is a development platform that provides a powerful API and management console for building backend servers for web and mobile applications. To install it, run the command below:

npm install appwrite
Enter fullscreen mode Exit fullscreen mode

Setting up Appwrite

To get started, we need to log into our Appwrite console, click the Create project button, input recipe_app as the name, and then click Create.

create project

Create a Database, Collection, and Add Attributes
With our project created, we can set up our application database. First, navigate to the Database tab, click the Create database button, input recipes as the name, and then click Create.

Create database

Secondly, we need to create a collection for storing our recipes. To do this, click the Create collection button, input recipe_collection as the name, and then click Create.

Create collection

Thirdly, we need to create attributes to represent our database fields. To do this, we need to navigate to the Attributes tab and create Attributes for each of the values shown below:

Attribute key Attribute type Size Required
title String 250 YES
ingredients String 5000 YES
direction String 5000 YES

Create attribute
List of attributes

Lastly, we need to update our database permission to manage them accordingly. To do this, we need to navigate to the Settings tab, scroll to the Update Permissions section, select Any, mark accordingly, and then click Update.

select any
mark permission

Building the Recipe App

To get started, first, we need to create a components/utils.js to abstract the application logic from the UI and add the snippet below:

import { Client, Databases, Account, ID } from 'appwrite';

const PROJECT_ID = 'REPLACE WITH PROJECT ID';
const DATABASE_ID = 'REPLACE WITH DATABASE ID';
const COLLECTION_ID = 'REPLACE WITH COLLECTION ID';

const client = new Client();
const databases = new Databases(client);

client.setEndpoint('http://localhost/v1').setProject(PROJECT_ID);

export const account = new Account(client);

export const create = (data) =>
    databases.createDocument(DATABASE_ID, COLLECTION_ID, ID.unique(), data);

export const getList = () =>
    databases.listDocuments(DATABASE_ID, COLLECTION_ID);
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Initializes Appwrite client and databases with required arguments
  • Creates account, create, and getList functions for managing user sessions, creating a recipe, and getting the list of recipes

PS: We can get the required IDs on our Appwrite Console.

Secondly, we need to create a modal.vue component inside the components folder for creating a recipe and add the snippet below:

modal.vue Logic

<template>
<!-- markup goes here -->
</template>

<script setup>
import { create } from "./utils";
const props = defineProps(["onModalChange"]);

//state
const isLoading = ref(false);
const isError = ref(false);

const form = reactive({
  title: "",
  ingredients: "",
  direction: "",
});

const onSubmit = () => {
  isLoading.value = true;
  isError.value = false;
  create({
    title: form.title,
    ingredients: form.ingredients,
    direction: form.direction,
  })
    .then((res) => {
      isLoading.value = false;
      isError.value = false;
      props.onModalChange(false);
      window.location.reload();
    })
    .catch((_) => {
      isLoading.value = false;
      isError.value = true;
    });
};
</script>
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports required dependency and defines required props
  • Creates application state and form elements
  • Creates an onSubmit method that uses the create helper function to create recipes and update states accordingly

modal.vue UI

<template>
  <div
    class="
      h-screen
      w-screen
      bg-cyan-900 bg-opacity-30
      z-30
      top-0
      fixed
      transform
      scale-105
      transition-all
      ease-in-out
      duration-100
    "
  >
    <div
      class="flex flex-col justify-center items-center h-full w-full open-nav"
    >
      <section
        class="
          w-11/12
          lg:w-1/2
          2xl:w-6/12
          bg-white
          flex
          justify-center
          items-center
          mt-5
          rounded-lg
        "
      >
        <div class="w-11/12 py-8">
          <div v-if="isError" class="text-center text-red-700">
            Error creating recipe
          </div>
          <div class="flex justify-between items-center mb-4">
            <h2 class="capitalize text-xl text-gray-500 font-medium">
              create recipe
            </h2>
            <button
              class="text-xs text-gray-700 hover:bg-gray-400"
              @click="props.onModalChange(false)"
            >
              Close
            </button>
          </div>
          <form @submit.prevent="onSubmit">
            <fieldset class="mb-4">
              <label class="text-sm text-gray-400 mb-2 block">Title</label>
              <input
                type="text"
                name="title"
                required
                placeholder="title"
                class="w-full h-10 border border-gray-400 rounded-sm px-4"
                v-model="form.title"
              />
            </fieldset>
            <fieldset class="mb-4">
              <label class="text-sm text-gray-400 mb-2 block"
                >Ingredients</label
              >
              <textarea
                rows="3"
                name="ingredients"
                required
                placeholder="rice, beans, ...."
                class="w-full border border-gray-400 rounded-sm px-4"
                v-model="form.ingredients"
              />
            </fieldset>
            <fieldset class="mb-4">
              <label class="text-sm text-gray-400 mb-2 block">Direction</label>
              <textarea
                rows="3"
                name="direction"
                required
                placeholder="details..."
                class="w-full border border-gray-400 rounded-sm px-4"
                v-model="form.direction"
              />
            </fieldset>
            <button
              class="
                text-white
                capitalize
                px-6
                py-2
                bg-cyan-900
                rounded-md
                w-full
              "
              :disabled="isLoading"
            >
              save
            </button>
          </form>
        </div>
      </section>
    </div>
  </div>
</template>

<script setup>
// app logic goes here
</script>
Enter fullscreen mode Exit fullscreen mode

The snippet above uses the declared state to show error conditionally and bind form elements.

Lastly, we need to update the app.vue file as shown below:

app.vue Logic

<template>
<!-- markup goes here -->
</template>

<script setup>
import { getList, account } from "./components/utils";

//state
const isLoading = ref(false);
const isError = ref(false);
const isModal = ref(false);
const recipes = ref(null);

const onModalChange = (state) => {
  isModal.value = state;
};

onMounted(async () => {
  isLoading.value = true;
  isError.value = false;
  //check session
  account
    .get()
    .then()
    .catch((_) => account.createAnonymousSession());
  //make api call
  getList()
    .then((res) => {
      recipes.value = res;
      isLoading.value = false;
      isError.value = false;
    })
    .catch((_) => {
      isLoading.value = false;
      isError.value = true;
    });
});
</script>
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Imports the required dependency
  • Creates application states and an onModalChange method to manage the visibility
  • Conditionally checks if a user has a valid session using the account helper function and gets the list of recipes using the getList helper function

app.vue UI


<template>
  <div>
    <nav class="h-16 border-b flex justify-between items-center px-3 mb-10">
      <h2 class="font-bold text-xl">Recipe app</h2>
      <button
        class="
          text-sm
          font-bold
          bg-cyan-900
          hover:bg-cyan-700
          text-white
          rounded
          px-5
          py-2
        "
        @click="onModalChange(true)"
      >
        Create
      </button>
    </nav>
    <section class="flex justify-center">
      <div v-if="isLoading" class="w-full lg:w-2/4">Loading ...</div>
      <div v-if="isError" class="w-full lg:w-2/4 text-red-700">
        Error Loading Recipes
      </div>
      <ul v-if="recipes" class="w-full lg:w-2/4">
        <li
          v-for="recipe in recipes.documents"
          :key="recipe.$id"
          class="px-4 py-2 border rounded-lg mb-5"
        >
          <div class="border-b h-8 mb-4">
            <h3 class="text-gray-700 font-bold">{{ recipe.title }}</h3>
          </div>
          <div class="mb-4">
            <p class="text-xs text-gray-500 mb-2">Ingredients:</p>
            <p class="text-xs ml-2">
              {{ recipe.ingredients }}
            </p>
          </div>
          <div class="bg-cyan-50 p-4 rounded">
            <p class="text-xs text-gray-700 mb-2">Instruction:</p>
            <p class="text-xs text-gray-700 ml-2">
              {{ recipe.directions }}
            </p>
          </div>
        </li>
      </ul>
    </section>
  </div>
  <Modal v-if="isModal" :onModalChange="onModalChange" />
</template>

<script setup>
// app logic goes here
</script>
Enter fullscreen mode Exit fullscreen mode

The snippet above does the following:

  • Conditionally shows the list of recipes
  • Uses the Modal component and passes in the required props

With that done, we can start a development server using the command below:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Create recipe
List of recipe

We can validate the saved recipes by checking our collection on Appwrite.

Recipe collection

P.S.: This demo is a base implementation to demonstrate Appwrite's support for scaffolding a working prototype. Appwrite gives developers the magic wound to extend the application to include multiple fields, separate collections, authentication, etc.

Conclusion

This post discussed creating a recipe app in Nuxt.js using TailwindCSS and Appwrite. The Appwrite platform allows developers the required experience and SDKs to build medium to large enterprise applications.

These resources might be helpful:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .