Image upload widgets allow users to preview images they upload to an application or database via forms, Google Drive, etc. These widgets enable the user to decide to go through or delete the images chosen for submission.
What we will be building
This post discusses uploading images using the Cloudinary image upload widget and storing the resulting image URL on the Appwrite database to create an e-commerce product catalog.
GitHub URL
https://github.com/Iheanacho-ai/optimised-image-upload-vue
Prerequisites
To get the most out of this project, the following are required:
- A basic understanding of CSS, JavaScript, and Vue.js.
- Docker Desktop is installed on the computer, run the
docker -v
command to verify that we have Docker Desktop installed; if not, install it from the Get Docker documentation. - An Appwrite instance running on our computer. Check out this article to create a local Appwrite instance; we will use Appwrite’s robust database and Realtime service to manage our application.
Setting up our Vue.js application
We install the Vue CLI by running these terminal commands to create a new Vue project.
npm install -g @vue/cli
# OR
yarn global add @vue/cli
After installing the Vue CLI, we navigate to our preferred directory and create a new project.
vue create <name of our project>
We change the directory to the project and start a development server with:
npm run serve
To see the app, we go to http://localhost:8080/
Installing dependencies
Installing Tailwind CSS
Tailwind CSS is a "utility-first" CSS framework that allows us to rapidly create user interfaces for web applications.
To install Tailwind CSS in our project, we run these terminal commands:
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
These commands create two files in the root directory of our project, tailwind.config.js
and postcss.config.js
.
In our tailwind.config.js
, we add the paths to all our template files with this code below.
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
content: [],
theme: {
extend: {},
},
plugins: [],
}
Next, we add the tailwind directives in our src/index.css
file.
@tailwind base;
@tailwind components;
@tailwind utilities;
Installing Appwrite
Appwrite Appwrite is an open-source, end-to-end, back-end server solution that allows developers to build applications faster.
To use Appwrite in our Vue application, we install the Appwrite client-side SDK for web applications.
npm install appwrite
Creating a new Appwrite project
During the creation of the Appwrite instance, we specified what hostname and port we use for our console. The default value is localhost:80.
We go to localhost:80 and create a new account to see our console.
On our console, there is a Create Project button. Click on it to start a new project.
Our project dashboard appears once we have created the project. At the top of the page, there is a Settings bar. Click it to access the Project ID and API Endpoint.
We copy the Project ID and API Endpoint, which we need to initialize the Appwrite Web SDK.
We create an init.js
file in our project's root directory to initialize the Appwrite Web SDK with the following code.
import { Appwrite } from 'appwrite';
export const sdk = new Appwrite();
sdk
.setEndpoint('http://localhost/v1') // Replace this with your endpoint
.setProject('projectID'); // Replace this with your ProjectID
Creating an anonymous user session
Appwrite requires a user to sign in before reading or writing to a database to enable safety in our application. However, they allow us to create an anonymous session to bypass that policy for simple projects. We'll do so in our init.js
file.
import { Appwrite } from 'appwrite';
export const sdk = new Appwrite();
sdk
.setEndpoint('http://localhost/v1') // Replace this with your endpoint
.setProject(projectID); // Replace this with your ProjectID
// Create an anonymous user session
sdk.account.createAnonymousSession().then(
(response) => {
console.log(response);
},
(error) => {
console.log(error);
}
);
Creating the collection and attributes
Next, we set up our database that will store our order status. In the Appwrite web Console, we click on Database on the left side of the dashboard.
We create a collection in our database tab by clicking on the Add Collection button. This action redirects us to a Permissions page.
At the Collection Level, we want to assign a Read Access and Write Access with a role:all value. We can modify the permissions to specify who has access to read or write to our database.
On the right of the Permissions page, we copy the Collection ID, which we need to perform operations on the collection’s documents.
Next, we go to our Attributes tab to create the properties we want a document to have.
We create three string attributes: productName, productPrice, and productImage.
Creating our product creation page
We create our product creation page in the App.vue
file. This product creation page will contain two sections: the first includes a form to collect product information, and the second section lists the products in our database.
Here, we'll create a form to submit the product's name, price, and image to the database. We add the following code snippet in the index.js
file to create the form styled with Tailwind CSS.
<template>
<div class= 'product-catalog'>
<div class="product-container mt-5 md:mt-0 md:col-span-2">
<div class="shadow sm:rounded-md sm:overflow-hidden">
<div class="px-4 py-5 bg-white space-y-6 sm:p-6">
<div>
<label for="price" class="block text-sm font-medium text-gray-700">Product Name</label>
<div class="mt-1 relative rounded-md shadow-sm">
<input type="text" name="price" v-model="productName" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md" placeholder="product name" />
</div>
</div>
<div>
<label for="price" class="block text-sm font-medium text-gray-700">Price</label>
<div class="mt-1 relative rounded-md shadow-sm">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span class="text-gray-500 sm:text-sm"> $ </span>
</div>
<input type="text" name="price" v-model="productPrice" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md" placeholder="0.00" />
</div>
</div>
<button nclass="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Upload files
</button>
<div className="px-4 py-3 bg-gray-50 text-right sm:px-6">
<button
type="button"
className="cursor inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
</div>
</div>
</div>
</div>
</div>
</template>
In the style
section of our App.vue
file, we build on the Tailwind CSS styles with these CSS styles.
<style>
.product-container{
margin-left: 37%;
width: 30%;
}
</style>
Here's how our product creation form looks.
Embedding the Cloudinary Upload widget
In this project, the Upload File button opens up the Cloudinary image upload widget to allow us to upload images to the Appwrite database.
To understand how to embed the Cloudinary image upload widget in our project, check out this article.
Add interaction with our database
When a user clicks on the save button, we want to store the product items on the Appwrite database and list the stored items in our application.
Storing the product information in the database
We store information on the Appwrite database as documents. In the script section of our App.vue
file, we create four variables in the data
object.
<script>
export default {
name: 'App',
data(){
return{
productName: '',
productPrice: '',
productImage: '',
products: []
}
},
}
</script>
The variables hold the following information.
- The
productName
variable holds the name of the product to be stored in the database - The
productPrice
variable contains the price of the product to be stored in the database - The
productImage
variable holds the image URL of the product to be stored in the database - The
products
variable is an array of all the products stored in the database
Next, we import the sdk
instance from our init.js
file into our App.vue
file.
import {sdk} from '../init';
Then, we create a handleProductSubmit
function in our App.vue
file to create documents on our database.
handleProductSubmit: async function(){
try {
await sdk.database.createDocument(collectionID, 'unique()', {
"productName" : this.productName,
"productPrice": this.productPrice,
"productImage": this.productImage
});
alert('your job item has been successfully saved')
this.productName= '',
this.productPrice= '',
this.productImage= ''
} catch (error) {
console.log(error)
}
},
The handleProductSubmit
function does the following:
- Creates a new document using Appwrite’s
createDocument()
function while passing the collection ID and attribute values as parameters - Alerts us when we have successfully saved the document, then cleans out the information in the local state variables
- Logs any error encountered during document creation to our console
https://gist.github.com/Iheanacho-ai/d20d8322de5e1d51d0ad89febeac996e
Listing the products
In the App.vue
file, we create a listProducts
function to fetch the product information stored in our database.
listProducts: async function(){
try {
let response = await sdk.database.listDocuments('628a9019078ea3c2b384');
this.products = response.documents
} catch (error) {
console.log(error)
}
},
The listProducts
function does the following:
- Lists all the documents in the collection
- Saves the documents in the
products
array - Logs any error encountered to the console
We then call the listProducts
function in our handleProductSubmit
function and the mounted
lifecycle.
mounted: function(){
this.listProducts()
},
mounted: function(){
handleProductSubmit: async function(){
try {
...
this.listProducts()
} catch (error) {
console.log(error)
}
},
}
Deleting the products
Next, we create a handleDelete
function in our App.vue
file to delete any product and its information from our Appwrite database.
handleDelete: async function(documentid){
try {
await sdk.database.deleteDocument(collectionID, documentid);
alert("item have been deleted successfully")
this.listProducts()
} catch (error) {
console.log(error)
}
}
This handleDelete
function does the following:
- Deletes a document from our Appwrite database collection with the
deleteDocument
function. ThisdeleteDocument
function finds the document using its document ID and collection ID parameter. - Alerts the user when we have deleted a document successfully.
- Lists the remaining product on our database using the
listProducts
function. - Logs any error encountered during document deletion.
After creating our functions, here is how our App.vue
file looks.
https://gist.github.com/Iheanacho-ai/7a7eb7a1649bfc492cf9e3282101fcb8
Next, we pass the productName
and productPrice
variables to our form's input
elements.
<input type="text" name="price" v-model="productName" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md" placeholder="product name" />
<input type="text" name="price" v-model="productPrice" class="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-7 pr-12 sm:text-sm border-gray-300 rounded-md" placeholder="0.00" />
Finally, we pass the handleProductSubmit
function to our Save button.
<button
type="button"
@click="handleProductSubmit"
className="cursor inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
Save
</button>
NOTE: We use a button with a type= button
to override the button’s default submit behavior.
After completing the steps thus far, here is how our App.vue
file looks.
https://gist.github.com/Iheanacho-ai/8f5106c552eee8cf1d4cd4efc8a3d5fa
Creating the product listing user interface
To create the user interface for displaying the products stored on the Appwrite database, we paste this code into our App.vue
file.
<div className="bg-white">
<div className="max-w-2xl mx-auto py-16 px-4 sm:py-24 sm:px-6 lg:max-w-7xl lg:px-8">
<h2 className="sr-only">Products</h2>
<div className="grid grid-cols-1 gap-y-10 sm:grid-cols-2 gap-x-6 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
<a href="#" v-for= 'product in products' :key= 'product.productName' className="group">
<div className="w-full aspect-w-1 aspect-h-1 bg-gray-200 rounded-lg overflow-hidden xl:aspect-w-7 xl:aspect-h-8">
<img :src="product.productImage" alt="Tall slender porcelain bottle with natural clay textured body and cork stopper." className="w-full h-full object-center object-cover group-hover:opacity-75" />
</div>
<h3 className="mt-4 text-sm text-gray-700">{{product.productName}}</h3>
<p className="mt-1 text-lg font-medium text-gray-900">${{product.productPrice}}</p>
<button
type="button"
className="cursor inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
@click="handleDelete(product.$id)"
>
Delete
</button>
</a>
</div>
</div>
</div>
In the code block above, we:
- Loop through the
products
array to render each product. - Pass in the
productName
,productPrice
, andproductImage
variables on theproducts
array. - Pass the
handleDelete
function and document ID parameter to a@Click
event listener on the Deletebutton
.
Here is how the App.vue
file looks.
https://gist.github.com/Iheanacho-ai/03f5eb3f75fa8e799fd217c23229b97e
Fill out the form to see how the product catalog looks.
Conclusion
This article discussed managing optimized image upload with Cloudinary and storing the images on an Appwrite database. Using this information, we created a product catalog. You can modify the fields to accommodate more product information.
Resources
Here are some resources that might be helpful.