In the previous part of this series, I introduced Appwrite and explained multiple definitions and logic. In case you didn't read the previous article and you don't know what is BaaS or Appwrite, make sure to check it out to have a better understanding.
In this series, we're going to code and utilize amazing Appwrite features to build a complete E-Commerce website. Every part of the series will cover one or more parts of our website.
🚨 Prerequisites:
- Do you like looking directly at source code? Here is the source code for this tutorial:
MooseSaeed / vue-appwrite-e-commerce
Appwrite E-Commerce Tutorial
Appwrite E-Commerce Tutorial
- I have created Github branches for each section in case you would like to look at the source code for this specific section only.
- We're building this Appwrite E-Commerce website with VueJS and Tailwind CSS. A prior knowledge of these technologies will definitely help you understand this tutorial better. If you don't have any background in regards to Tailwind or Vue, don't worry about it and just follow along and try to keep you mind open.
Project setup
npm install
Compiles and hot-reloads for development
npm run serve
Compiles and minifies for production
npm run build
Customize configuration
- I have created Github branches for each section in case you would like to look at the source code for this specific section only.
We're building this Appwrite E-Commerce website with VueJS and TailwindCSS. A prior knowledge of these technologies will definitely help you understand this tutorial better. If you don't have any background in regards to Tailwind or Vue, don't worry about it, just follow along and try to keep you mind open.
My main focus during this tutorial is Appwrite integration and features utilization.
1- E-Commerce Frontend Skeleton:
I have grabbed multiple components online into VueJS 3 project to have my tailwind frontend ready with what I need. Mainly the design is fetched from a free E-Commerce Tailwind product on Gumbroad by Halit Güvenilir.
This will give me the kick start I need to use Appwrite and work with its APIs. I have done modifications to the original design to fit my tutorial, if you would like to have a copy of the final skeleton, Just head to the tutorial repo on Github and fork the master branch.
Keep in mind that I might do several changes to the design as we go through our tutorial, this depends on what exactly we're trying to accomplish.
Feel free to explore the repo as I already set up few routes for the authentication page, product view page, admin overview page, add new product page and cart page:
src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import Homepage from "../components/main/Homepage.vue";
import Productview from "../components/main/Productview.vue";
import Authpage from "../components/main/Authpage.vue";
import Adminpanel from "../components/main/Adminpanel.vue";
import Newproduct from "../components/main/Newproduct.vue";
import Mycart from "../components/main/Mycart.vue";
const routes = [
{
path: "/",
name: "Homepage",
component: Homepage,
},
{
path: "/products/example",
name: "Productview",
component: Productview,
},
{
path: "/auth",
name: "Authpage",
component: Authpage,
},
{
path: "/admin",
name: "Adminpanel",
component: Adminpanel,
},
{
path: "/newproduct",
name: "Newproduct",
component: Newproduct,
},
{
path: "/mycart",
name: "Mycart",
component: Mycart,
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
return {
top: 0,
behavior: "smooth",
};
},
});
export default router;
2- Install Appwrite with VueJS:
After we finally have our frontend Tailwind sekelton, now it's time to install Appwrite in our project.
As mentioned in the previous article, Appwrite Installation Docs is very descriptive and has plenty of options, but if You would like another source of information you can check out this article by @lohanidamodar
I simply installed Docker (I'm working on Windows 10) and in the project folder.
- I ran the below PowerShell command to install Appwrite according to the documentation:
docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.14.2
After PowerShell command is done, It will ask you about the server HTTP port and other things. I will just stick with the defaults without changing anything. This eventually will allow you to open Appwrite's console on port 80 by default (localhost:80).
Enter in the browser
localhost:80
and signup to Appwrite's console. Press on create project and choose a name and ID for your project. In my case I named itappwrite-ecom
.Following the documentation to install Web SDK, I hit the below command in the project folder.
npm install appwrite
- I created
.js
file to import Appwrite Web SDK in order to be able to use it in my VueJS components.
src/utils.js
import { Appwrite } from "appwrite";
const appwrite = new Appwrite();
appwrite
.setEndpoint("http://localhost/v1") // Your API Endpoint
.setProject("appwrite-ecom"); // Your project ID
export { appwrite };
- If you would like to start coding with me from here, simply head to the Github repo and fork the appwrite-installed branch to have your own copy of this project with appwrite installed.
3- Authentication:
Now, It's time to start working on our E-Commerce website. The first thing to do is to set up authentication for website's users. Appwrite made this task extremely easy for developers with their Web SDK.
Let's head to src/components/main/Authpage.vue
, You will find that it's only a VueJS component where the template includes some code to show signin/signup forms.
Authpage.vue
<template>
<div>
<Login v-if="!pageSwitch">
<div>
<p>
{{ pageSwitch ? "Got an account?" : "Haven't got an account?" }}
<span
class="cursor-pointer"
@click="() => (this.pageSwitch = !this.pageSwitch)"
>{{ pageSwitch ? "Login" : "Sign Up" }}</span
>
</p>
</div>
</Login>
<Signup v-if="pageSwitch">
<div>
<p>
{{ pageSwitch ? "Got an account?" : "Haven't got an account?" }}
<span
class="cursor-pointer"
@click="() => (this.pageSwitch = !this.pageSwitch)"
>{{ pageSwitch ? "Login" : "Sign Up" }}</span
>
</p>
</div>
</Signup>
</div>
</template>
In the above code, we're using a Vue click event to show and hide the forms upon the user click. If the user clicked on 'Sign Up' show the sign up form, and if the user clicked on 'Sign In' show the sign in form.
Script tags only contains the components we're using and setting pageSwitch to false to show the Sign in form by default.
<script>
import Login from "../Login.vue";
import Signup from "../Signup.vue";
export default {
name: "Authpage",
components: {
Login,
Signup,
},
data: () => {
return {
pageSwitch: false,
};
},
};
</script>
We need now to add few methods using Appwrite's Web SDK to allow users to login and signup. We have this parent component Authpage.vue
that will include all the methods while the other child components Login.vue
and Signup.vue
will only pass the required data from the forms to the parent component to trigger it's methods.
First thing, let's create a method to check if the user already logged in or not as soon as they open the page. This will help us provide a response once the user login or signup. In our case, we want to save the logged in user details in order to fetch them whenever we want and take the user back to the page they were on before logging in.
- We need to create
src/store.js
file to store the user details in as a shared information across all components in case we needed it in any of them.
store.js
import { reactive } from "vue";
export const store = reactive({
userprofile: false,
});
- Let's import Appwrite from
src/utils.js
in our Authpage.vue file:
Authpage.vue
import appwrite from "../../utils";
Now we're going to use appwrite's method Get Account to check if the user is logged in, if yes then save it to store.js
and take the user back to the page they were browsing before, if no let me know the error in the console.
To trigger this checkLogin method that we're creating, I have used Vue lifecycle mounted hook.
The script now will look like this:
Authpage.vue
<script>
import Login from "../Login.vue";
import Signup from "../Signup.vue";
import { appwrite } from "../../utils";
import { store } from "../../store.js";
export default {
name: "Authpage",
components: {
Login,
Signup,
},
data: () => {
return {
pageSwitch: false,
};
},
mounted: function () {
this.checkLogin();
},
methods: {
async checkLogin() {
try {
// Getting the currently logged in account
const response = await appwrite.account.get();
// If there is a response, save it to store.js
store.userprofile = response;
// Once action done, get the user back to the previous page.
this.$router.go(-1);
} catch (err) {
if (err == "Error: Unauthorized") return;
console.error(err);
}
},
},
};
</script>
Now if you head in your browser to /auth
which is a route already set in router/index.js
you will see the below appwrite response error in the console because there isn't any logged in user to check:
User (role: guest) missing scope (account)
at Appwrite.eval
let's create the first method for signing up. According to Appwrite documentation, we need to create an account and we're able to pass userId, email, password and name. In my case, Instead of passing a userId I will allow users to insert a username in the signup form and I will use this username as a userId.
Also we need to create a session for this logged in user.
Let's create our method and get it ready to receive the information we need to pass to Appwrite:
Authpage.vue
async signup(username, nameOfUser, password, email) {
try {
await appwrite.account.create(username, email, password, nameOfUser);
await appwrite.account.createSession(email, password);
this.checkLogin();
} catch (error) {
console.error(error);
return false;
}
},
Now let's continue to have all of our methods ready the same way we did with signup method, eventually we will have this result:
Authpage.vue
<template>
<div>
<div v-if="!store.userprofile">
<Login v-if="!pageSwitch">
<div>
<p>
{{ pageSwitch ? "Got an account?" : "Haven't got an account?" }}
<span
class="cursor-pointer"
@click="() => (this.pageSwitch = !this.pageSwitch)"
>{{ pageSwitch ? "Login" : "Sign Up" }}</span
>
</p>
</div>
</Login>
<Signup v-if="pageSwitch">
<div>
<p>
{{ pageSwitch ? "Got an account?" : "Haven't got an account?" }}
<span
class="cursor-pointer"
@click="() => (this.pageSwitch = !this.pageSwitch)"
>{{ pageSwitch ? "Login" : "Sign Up" }}</span
>
</p>
</div>
</Signup>
</div>
</div>
</template>
<script>
import { appwrite } from "../../utils";
import { store } from "../../store.js";
import Login from "../Login.vue";
import Signup from "../Signup.vue";
export default {
name: "Authpage",
components: {
Login,
Signup,
},
data: () => {
return {
pageSwitch: false,
store,
};
},
mounted: function () {
this.checkLogin();
},
methods: {
// Logout the current logged in user in store.js
async logout() {
store.userprofile = false;
appwrite.account.deleteSession("current");
},
async login() {
try {
await appwrite.account.createSession(
event.target.email.value,
event.target.password.value
);
this.checkLogin();
return true;
} catch (error) {
console.error(error);
return false;
}
},
async signup(username, nameOfUser, password, email) {
try {
await appwrite.account.create(username, email, password, nameOfUser);
await appwrite.account.createSession(email, password);
this.checkLogin();
} catch (error) {
console.error(error);
return false;
}
},
// Using username to have a dynamic routes for user profiles
async updateUsername(username) {
try {
await appwrite.account.updatePrefs({
username: username,
});
} catch (error) {
console.error(error);
return false;
}
},
async checkLogin() {
try {
const response = await appwrite.account.get();
store.userprofile = response;
// Once action done, get the user back to the previous page.
this.$router.go(-1);
} catch (err) {
if (err == "Error: Unauthorized") return;
console.error(err);
}
},
},
};
</script>
Now in the src/components/Login.vue
file, We're going to trigger the parent's login method and inject it with the required data from the form.
Login.vue
<template>
<main class="relative">
<div class="px-4 py-10 mx-auto sm:px-6 lg:px-8">
<div class="max-w-lg mx-auto">
<h1 class="text-2xl font-bold text-center dark:text-white sm:text-3xl">
Login
</h1>
<p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
<form
@submit="processLogin"
action=""
class="p-8 mt-6 mb-0 space-y-4 rounded-xl shadow-2xl dark:bg-gray-700/50 backdrop-blur-md bg-none dark:text-white"
>
<p class="text-lg font-medium">Login to your account</p>
<div>
<label for="email" class="text-sm font-medium">Email</label>
<div class="relative mt-1">
<input
v-model="email"
type="email"
id="email"
required
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
placeholder="Enter email"
/>
</div>
</div>
<div>
<label for="password" class="text-sm font-medium">Password</label>
<div class="relative mt-1">
<input
v-model="password"
type="password"
id="password"
required
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
placeholder="Enter password"
/>
</div>
</div>
<button
class="px-4 py-2 text-center w-full inline-block text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
type="submit"
>
Sign in
</button>
<slot />
</form>
</div>
</div>
</main>
</template>
<script>
export default {
name: "Login",
data: () => {
return {
email: "",
password: "",
error: false,
};
},
methods: {
async processLogin(e) {
e.preventDefault();
this.error = false;
// Validation
if (!(this.password.length >= 6 && this.password.length <= 32)) {
this.error = "Error: Password must be between 6 and 32 characters.";
return;
}
if ((await this.$parent.login()) === false) {
this.error = "Incorrect Credentials!";
}
},
},
};
</script>
We'll do the same for src/components/Signup.vue
:
Signup.vue
<template>
<main class="relative">
<div class="max-w-screen-xl px-4 py-10 mx-auto sm:px-6 lg:px-8">
<div class="max-w-lg mx-auto">
<h1 class="text-2xl font-bold text-center text-black sm:text-3xl">
Sign up
</h1>
<p v-if="error" class="text-red-500 text-sm">{{ error }}</p>
<form
@submit="processRegister"
action=""
class="p-8 mt-6 mb-0 space-y-4 rounded-lg shadow-2xl dark:bg-gray-700/50 backdrop-blur-md bg-none dark:text-white"
>
<p class="text-lg font-medium">Sign up new account</p>
<div>
<label for="nameOfUser" class="text-sm font-medium">Username</label>
<div class="relative mt-1">
<input
v-model="username"
type="text"
id="username"
required
placeholder="Enter username"
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
/>
</div>
</div>
<div>
<label for="nameOfUser" class="text-sm font-medium"
>Your Name</label
>
<div class="relative mt-1">
<input
v-model="nameOfUser"
type="text"
id="nameOfUser"
required
placeholder="Enter your name"
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
/>
</div>
</div>
<div>
<label for="email" class="text-sm font-medium">Email</label>
<div class="relative mt-1">
<input
v-model="email"
type="email"
id="email"
required
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
placeholder="Enter email"
/>
</div>
</div>
<div>
<label for="password" class="text-sm font-medium">Password</label>
<div class="relative mt-1">
<input
v-model="password"
type="password"
id="password"
required
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
placeholder="Enter password"
/>
</div>
</div>
<div>
<label for="confirmPassword" class="text-sm font-medium"
>Confirm Password</label
>
<div class="relative mt-1">
<input
v-model="confirmPassword"
type="password"
id="confirmPassword"
required
placeholder="Confirm Password"
class="appearance-none border border-gray-200 bg-gray-100 rounded-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400 w-full"
/>
</div>
</div>
<button
class="px-4 py-2 text-center w-full inline-block text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
type="submit"
>
Sign up
</button>
<slot />
</form>
</div>
</div>
</main>
</template>
<script>
export default {
name: "Signup",
data: () => {
return {
username: "",
nameOfUser: "",
email: "",
password: "",
confirmPassword: "",
loading: "",
error: false,
};
},
methods: {
async processRegister(e) {
e.preventDefault();
// Validation
if (this.loading) {
return;
}
// Password confirmation
if (this.password !== this.confirmPassword) {
this.error = "Error: Passwords must be matching.";
return;
}
// Length Validation
if (!(this.password.length >= 6 && this.password.length <= 32)) {
this.error = "Error: Password must be between 6 and 32 characters.";
return;
}
if (this.nameOfUser.length >= 100) {
this.error = "Error: Name can not exceed 100 characters";
return;
}
if (this.username.length >= 50) {
this.error = "Error: username can not exceed 50 characters";
return;
}
this.loading = true;
//I need a username here to make dynamic users routes
let username = this.username;
username = username.replace(/\s+/g, "").toLowerCase();
if (
(await this.$parent.signup(
username,
this.nameOfUser,
this.password,
this.email
)) === false
) {
this.error =
"Something went wrong while registering, Check console for more details.";
} else {
setTimeout(() => {
this.$parent.updateUsername(username);
}, 300);
}
},
},
};
</script>
Now our authentication system is working perfectly fine, you can text this by heading to /auth and click on signup then insert whatever details you want and Voilà ! You're logged in 🥳
Now we need to head to src/components/Header.vue
and show My Cart button in the header only if the user is logged in and hide Sign In as well.
Header.vue
<template>
<header class="bg-white py-3">
<div class="container max-w-screen-xl mx-auto px-4">
<div class="flex flex-wrap items-center">
<!-- Brand -->
<div class="flex-shrink-0 mr-5 font-bold text-xl text-blue-700">
<a href="/"> E-Commerce </a>
</div>
<!-- Brand .//end -->
<!-- Search -->
<div
class="flex flex-nowrap items-center w-full order-last md:order-none mt-5 md:mt-0 md:w-2/4 lg:w-2/4"
>
<input
class="flex-grow appearance-none border border-gray-200 bg-gray-100 rounded-tl-md rounded-bl-md py-2 px-3 hover:border-gray-400 focus:outline-none focus:border-gray-400"
type="text"
placeholder="Search"
/>
<button
type="button"
class="px-4 py-2 inline-block text-blue-600 border border-gray-200 bg-gray-100 rounded-tr-md rounded-br-md hover:bg-blue-100"
>
<i class="fa fa-search"></i>
</button>
</div>
<!-- Search .//end -->
<!-- Actions -->
<div class="flex items-center space-x-2 ml-auto">
<a
v-if="store.userprofile"
class="px-3 py-2 inline-block text-center text-gray-700 bg-white shadow-sm border border-gray-200 rounded-md hover:bg-gray-100 hover:border-gray-300"
href="/mycart"
>
<i class="text-gray-400 w-5 fa fa-shopping-cart"></i>
<span class="hidden lg:inline ml-1">My cart</span>
</a>
<a
v-if="!store.userprofile"
class="px-3 py-2 inline-block text-center text-gray-700 bg-white shadow-sm border border-gray-200 rounded-md hover:bg-gray-100 hover:border-gray-300"
href="/auth"
>
<i class="text-gray-400 w-5 fa fa-user"></i>
<span class="hidden lg:inline ml-1">Sign in</span>
</a>
<a
class="px-3 py-2 inline-block text-center text-gray-700 bg-white shadow-sm border border-gray-200 rounded-md hover:bg-gray-100 hover:border-gray-300"
href="/admin"
>
<i class="text-gray-400 w-5 fa fa-user"></i>
<span class="hidden lg:inline ml-1">Admin</span>
</a>
<button
v-if="store.userprofile"
@click="logout"
class="px-3 py-2 inline-block text-center text-gray-700 bg-white shadow-sm border border-gray-200 rounded-md hover:bg-gray-100 hover:border-gray-300"
>
<i class="text-gray-400 w-5 fa fa-user"></i>
<span class="hidden lg:inline ml-1">logout</span>
</button>
</div>
<!-- Actions .//end -->
<!-- mobile-only -->
<div class="lg:hidden ml-2">
<button
type="button"
class="bg-white p-3 inline-flex items-center rounded-md text-black hover:bg-gray-200 hover:text-gray-800 border border-transparent"
>
<span class="sr-only">Open menu</span>
<i class="fa fa-bars fa-lg"></i>
</button>
</div>
<!-- mobile-only //end -->
</div>
<!-- flex grid //end -->
</div>
<!-- container //end -->
</header>
</template>
<script>
import { appwrite } from "../utils";
import { store } from "../store";
export default {
data() {
return {
store,
};
},
mounted() {
this.checkLogin();
},
methods: {
async checkLogin() {
try {
const response = await appwrite.account.get();
store.userprofile = response;
} catch (err) {
if (err == "Error: Unauthorized") return;
}
},
async logout() {
store.userprofile = false;
appwrite.account.deleteSession("current");
this.$router.push("/");
},
},
};
</script>
<style></style>
Now You can logout by pressing on the logout button and once you're logged out, you will see Sign in button again.
With that, we've finished our authentication system in our E-Commerce website and we're ready to start working on the shop, cart, products and admin panel and that's exactly what we're going to do in the next part of this series.
Let me know in the comments if you have any questions and I hope this was helpful for you :)
Top comments (0)