This project will be divided in three weeks:
-
Week 1:
- Mobile and Desktop layout;
- Responsive design;
- Shopping cart with state management;
- Product state management;
- Transitions and animations.
-
Week 2:
- Refactor last week's work;
- Use LocalStorage for cart data persistance while in guest view;
- Use DummyJson API to feed content to the page;
- Create sign up and login;
-
Week 3:
- Refactor last week's work;
- Use Firebase Auth for user authentication;
Use JWT tokens for user auth;
-
Week 4:
- Use Firebase Firestore to persist cart data;
- Create user profile page;
- Within user profile page:
- User should be able to delete account;
- User should be able to change name;
- When logged in user should have access to "Favorites" button, similar to the cart;
npm install
npm run dev
npm run build
- Vue.js 3
- TailwindCSS
- Responsive layout
I used Vue's built in component Transitions for the first time to create a better user experience.
Reusable transition for the pages when they are loaded, it is reausable because I abstracted it to component named SlideDownFade.Vue:
<script>
import { Transition } from 'vue';
</script>
<template>
<Transition name="slide-down-fade" appear>
<slot></slot>
</Transition>
</template>
<style>
.slide-down-fade-enter-from {
opacity: 0;
transform: translateY(-250px);
}
.slide-down-fade-enter-active {
transition: all 2s ease;
}
</style>
Here is SlideDownFade.Vue being used:
<template>
<SlideDownFade>
<div class="lg:mx-8 lg:flex lg:flex-wrap lg:justify-center">
<div>
<ProductCard v-for="product in mensShoes"
:key="product.id"
:brandName="product.brand"
:productName="product.title"
:finalCost="productsStore.getDiscountedPrice(product.id)"
:discount="product.discountPercentage"
:originalPrice="product.price"
:product="product" />
</div>
</div>
</SlideDownFade>
</template>
I learned how to synchronize data persistence (used localStorage) and state management, and it was very eazy once I had a store with all the logic I needed:
import { defineStore } from 'pinia';
import { useLocalStorage } from '@vueuse/core';
export const useCartStore = defineStore('cart', {
state: () => {
return {
toggleCart: false,
items: useLocalStorage('cart', [])
}
},
getters: {
cartItems(){
return this.items.reduce((acc, item) => acc + item.amount, 0)
}
},
actions: {
addToCart(product) {
const existingProduct = this.items.find(element => element.product.id === product.id);
if(existingProduct) {
existingProduct.amount = existingProduct.amount+1
} else {
this.items.push({product, amount: 1})
}
},
deleteItem(id) {
const productToBeDeleted = this.items.findIndex(element => element.product.id === id)
this.items.splice(productToBeDeleted, 1)
}
}
})
To consume the DummyJSON API, I decided to abstract the URL's that I needed in the config.js file:
let domain = 'https://dummyjson.com/products/category/'
export const apiDomain = domain;
export const mensShoesUrl = apiDomain + 'mens-shoes';
export const womensShoesUrl = apiDomain + 'womens-shoes';
export const featuredUrl = apiDomain + 'womens-shoes?limit=1';
And then the functions that get the JSON objects in the products.js file in the services folder:
import { featuredUrl, mensShoesUrl, womensShoesUrl } from '../config.js';
export async function getMensShoes() {
const request = mensShoesUrl;
const fetchResponse = await fetch(request);
const response = await fetchResponse.json();
return response.products;
}
export async function getWomensShoes() {
const request = womensShoesUrl;
const fetchResponse = await fetch(request);
const response = await fetchResponse.json();
return response.products;
}
export async function getFeaturedShoes() {
const request = featuredUrl;
const fetchResponse = await fetch(request);
const response = await fetchResponse.json();
return response.products;
}
export function getDiscountedPrice(id, products) {
const product = products.find(element => element.id === id);
const discountedPrice = product.discountPercentage > 0.0 ? product.price - ((product.price / 100) * product.discountPercentage) : product.price;
return discountedPrice.toFixed(2);
}
export function getImages(id, products) {
const product = products.find(element => element.id === id);
const images = product.images;
return images;
}
I learned that with Firebase Authentication SDK I don't need to worry about JWT tokens because the library already handles user login state (YAY!). It is also possible to use social login or Federated Identity Providers, that means that I can easily implement login with Google, Facebook, Github and many other providers.
const submitForm = async () => {
const result = await v$.value.$validate();
if (result) {
store.$patch({
name: formData.name,
email: formData.email
});
createUserWithEmailAndPassword(getAuth(), formData.email, formData.password)
.then((data) => {
router.push('/user-profile')
})
.catch((error) => {
alert(error.message);
})
} else {
animateButton();
router.push('/sign-up')
}
}
const signInWithGoogle = () => {
const provider = new GoogleAuthProvider();
signInWithPopup(getAuth(), provider)
.then((result) => {
router.push('/user-profile')
})
.catch((error) => {
animateButton();
if(error.code == 'auth/user-not-found') {
$externalResults.value = { email: 'User not found, please register.' }
}
if (error.code == 'auth/wrong-password'){
$externalResults.value = { email: 'User and/or password are incorrect' }
}
});
};
At first I wanted to use Firebase Realtime Database for the user data, but then a found out about a more recent solution called Firestore, so I decided to use that to store user information, such as cart, favorites and profile.
I learned how to use Firebase Storage to upload a profile picture.
import { getStorage, ref, uploadBytesResumable, getDownloadURL } from "firebase/storage";
export default function uploadProfilePicture(file, userId) {
const storage = getStorage();
// Create the file metadata
/** @type {any} */
const metadata = {
contentType: 'image/*'
};
// Upload file and metadata to the object 'images/mountains.jpg'
const storageRef = ref(storage, userId);
const uploadTask = uploadBytesResumable(storageRef, file, metadata);
// Listen for state changes, errors, and completion of the upload.
uploadTask.on('state_changed',
(snapshot) => {
// Get task progress, including the number of bytes uploaded and the total number of bytes to be uploaded
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
switch (snapshot.state) {
case 'paused':
console.log('Upload is paused');
break;
case 'running':
console.log('Upload is running');
break;
}
},
(error) => {
// A full list of error codes is available at
// https://firebase.google.com/docs/storage/web/handle-errors
switch (error.code) {
case 'storage/unauthorized':
// User doesn't have permission to access the object
break;
case 'storage/canceled':
// User canceled the upload
break;
// ...
case 'storage/unknown':
// Unknown error occurred, inspect error.serverResponse
break;
}
},
() => {
// Upload completed successfully, now we can get the download URL
getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
console.log('File available at', downloadURL);
});
location.reload()
}
);
}
- Vue.js 3 Animations & Transitions Tutorial - This tutorial from KoderHQ helped me understand the basics of Transitions/TransitionGroups and how they work.
- John Komarnicki - John's youtube channel is rich with valuable content and he's a great communicator. I strongly encourage anyone that wants to take their Vue.js development skills to the next level.
- Simple Local Storage implementation using Vue 3, Vueuse and Pinia with zero extra lines of code. - In Stephan Langeveld's blog, he shows a simple way to use localStorage with Pinia.
- Firebase Auth and Storage - The Net Ninja is one of the best channels por learning new technologies.
My husband @filipedanielski who is always supporting and encouraging me.