Review App Using Laravel 11 & Vue js 3 Composition API Part 4
In the fourth part of this tutorial, we will fetch and display all the products on the home page, view product details, and add the navbar menu so we can move between pages.
Add the home component
Inside the home component, we fetch all the products from the backend and send them to the product list component.
<template>
<ProductList :products="data.products" />
</template>
<script setup>
import axios from "axios"
import { onMounted, reactive } from 'vue'
import ProductList from "./products/ProductList.vue"
const data = reactive({
products: []
})
const fetchAllProducts = async () => {
try {
const response = await axios.get('https://darija-coding.com/api/products')
data.products = response.data.data
} catch (error) {
console.log(error)
}
}
onMounted(() => fetchAllProducts())
</script>
<style>
</style>
Add the product list component
Inside the product list component, we receive the products, loop through, and send each product to the product list item component.
<template>
<div class="row my-4">
<ProductListItem v-for="product in products"
:key="product.id" :product="product" />
</div>
</template>
<script setup>
import ProductListItem from "./ProductListItem.vue"
const props = defineProps({
products: {
type: Array,
required: true
}
})
</script>
<style>
</style>
Add the product list item component
Inside the product list item component, we receive the product, display the details, calculate the average of the ratings, and display it.
<template>
<div class="col-md-4 mb-2">
<div class="card h-100">
<img :src="product.image" alt="Product Image"
class="card-img-top"
>
<div class="card-body">
<div class="card-title">
{{ product.name }}
</div>
<p class="card-text">
{{ product.desc }}
</p>
<p>
<span class="fw-bold text-danger">
$ {{ product.price }}
</span>
</p>
<p v-if="product.reviews.length > 0">
<StarRating
v-model:rating="reviewAvg"
read-only
:star-size="24"
/>
</p>
<router-link :to="`product/${product.id}`"
class="btn btn-dark">
<i class="bi bi-eye"></i> View
</router-link>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import StarRating from 'vue-star-rating'
const props = defineProps({
product: {
type: Object,
required: true
}
})
//calculate the average of the ratings
const reviewAvg = computed(() => parseInt(props.product.reviews.reduce((acc, review) => acc + review.rating / props.product.reviews.length, 0)))
</script>
<style>
</style>
Add the product component
Inside the product component, we fetch the product, using the ID, display the details, calculate the average of the ratings, and display it.
Also, we display the reviews of this product and the add/update review form that we will add in the next tutorial.
<template>
<div class="row my-5" v-if="data.product">
<div class="col-md-10 mx-auto">
<div class="card mb-3">
<div class="row g-0">
<div class="col-md-4">
<img :src="data.product.image" alt="Product Image"
class="img-fluid rounded-start">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-text">
{{ data.product.name }}
</h5>
<p class="card-text">
{{ data.product.desc }}
</p>
<p>
<small class="text-body-secondary">
$ {{ data.product.price }}
</small>
</p>
<p v-if="data.product.reviews.length > 0">
<StarRating
v-model:rating="reviewAvg"
read-only
:star-size="24"
/>
</p>
</div>
</div>
</div>
</div>
<div v-if="data.product.reviews.length > 0">
<ReviewsList
:reviews="data.product.reviews"
@editReviewEvent="editReview"
@removeReviewEvent="deleteReview"
/>
</div>
<div class="my-3">
<div class="card">
<div class="card-header bg-white text-center">
<h5 class="mt-2" v-if="!data.reviewToUpdate.updating">Add a review</h5>
<h5 class="mt-2" v-else>Edit a review</h5>
</div>
<div class="card-body">
<AddReview
v-if="!data.reviewToUpdate.updating"
:product="data.product"
@reviewAdded="setProduct"
/>
<UpdateReview
v-else
:reviewToUpdate="data.reviewToUpdate.data"
:product="data.product"
@cancelUpdating="cancelUpdating"
@reviewUpdated="setProduct"
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed, onMounted, reactive } from 'vue'
import { useRoute } from 'vue-router'
import axios from 'axios'
import AddReview from '../reviews/AddReview.vue'
import UpdateReview from '../reviews/UpdateReview.vue'
import StarRating from 'vue-star-rating'
import ReviewsList from '../reviews/ReviewsList.vue'
import { useToast } from 'vue-toastification'
const route = useRoute()
const toast = useToast()
const data = reactive({
product: null,
reviewToUpdate: {
updating: false,
data: null
}
})
//calculate the average of the ratings
const reviewAvg = computed(() => parseInt(data.product.reviews.reduce((acc, review) => acc + review.rating / data.product.reviews.length, 0)))
const fetchAllProductById = async () => {
try {
const response = await axios.get(`https://darija-coding.com/api/product/${route.params.id}/show`)
data.product = response.data.data
} catch (error) {
console.log(error)
}
}
const setProduct = (newProductData) => {
data.product = newProductData
if(data.reviewToUpdate.updating) {
data.reviewToUpdate = {
updating: false,
data: null
}
}
}
const editReview = (review) => {
data.reviewToUpdate = {
updating: true,
data: review
}
}
const cancelUpdating = () => {
if(data.reviewToUpdate.updating) {
data.reviewToUpdate = {
updating: false,
data: null
}
}
}
const deleteReview = async (review_id) => {
if(confirm('are you sure you want to remove this review?')) {
try {
const response = await axios.post(`https://darija-coding.com/api/review/${data.product.id}/delete`,
{
review_id
}
)
data.product = response.data.data
toast.success('Review has been deleted successfully', {
timeout: 2000
})
} catch (error) {
console.log(error)
}
}
}
onMounted(() => fetchAllProductById())
</script>
<style>
</style>
Add the header component
Inside the Header Component, we have the navigation menu.
<template>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<router-link class="navbar-brand" to="/">
<i class="bi bi-star h1"></i>
</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mx-auto mb-2 mb-lg-0">
<li class="nav-item">
<router-link class="nav-link active" aria-current="page" to="/">
<i class="i bi-house"></i> Home
</router-link>
</li>
</ul>
</div>
</div>
</nav>
</template>
<script setup>
</script>
<style>
</style>