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
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
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
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: [],
};
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;
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'],
});
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
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 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.
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.
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 |
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.
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);
The snippet above does the following:
- Imports the required dependency
- Initializes Appwrite
client
anddatabases
with required arguments - Creates
account
,create
, andgetList
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>
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 thecreate
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>
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>
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 thegetList
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>
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
We can validate the saved recipes by checking our collection on Appwrite.
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: