![Building Ecommerce Website With PHP Lumen Laravel And Nuxtjs](https://webmobtuts.com/wp-content/uploads/2020/12/Building-Ecommerce-Website-With-PHP-Lumen-Laravel-And-Nuxtjs-2-800x400.jpg)
In this post we will work on the wishlist page and how to store user’s favorite products in the wishlist using local storage mechanism and displaying those wishlisted products in specific page we will design.
Wishlists provide many benefits for example it can help users to save their favorite products so that they come back at later time to purchase those products. Users can add as many products to their wishlists and can remove the products also from the wishlist. Now how to store the wishlist? There are two options, the first is storing them into mysql and the second approach is using browser local storage.
For the purpose of this tutorial i will use the browser local storage to store the wishlist product Ids. Then in the wishlist page we will retrieve the product Ids and sending them to server to retrieve the actual products data.
Updating The Product Model
Open the lumen project and update the Product model to include another attribute which specifies if this product in wishlist or not. So update app/Models/Product.php by adding this method at the end of the class:
public function getInWishlistAttribute() { return false; }
Next update the $appends property like so:
protected $appends = ["slug", "description_short", "title_short", "is_discount_active", "price_after_discount", "price_after_discount_numeric", "in_wishlist"];
Now in the product response you will see the in_wishlist property by default set to false. In the next section we will implement the adding and removing in the Nuxtjs project.
Adding and Removing In Wishlist
Switch to the Nuxtjs project so can prepare the wishlist functionality. To add and remove from wishlist we will update these components and pages:
- components/product-templates/ProductTemplateNormal.vue
- pages/index.vue
- p/_id/_slug/index.vue
Let’s first create some helper functions to use for the purpose of adding and removal functionality. In the helpers/ directory create a new file wishlist.js
helpers/wishlist.js
export const getProductsInWishlist = () => { if(localStorage.getItem("products_wishlist") != null) { return JSON.parse(localStorage.getItem("products_wishlist")); } else { return null; } } export const addToWishlist = (productId) => { let products = localStorage.getItem("products_wishlist"); if(products) { products = JSON.parse(products); if(products.find(i => i === productId) == undefined) { products.push(productId); localStorage.setItem("products_wishlist", JSON.stringify(products)); } } else { localStorage.setItem("products_wishlist", JSON.stringify([productId])); } }; export const removeFromWishlist = (productId) => { let products = localStorage.getItem("products_wishlist"); products = JSON.parse(products); const filtered = [...products.filter(i => i !== productId)]; localStorage.setItem("products_wishlist", JSON.stringify(filtered)); } export const clearWishlist = () => { localStorage.removeItem("products_wishlist"); } export const isProductInWishlist = (productId) => { let products = localStorage.getItem("products_wishlist"); if(products) { products = JSON.parse(products); if(products.find(i => i === productId) !== undefined) { return true; } return false; } return false; } export const updateWishlistProducts = (products) => { products.map(item => { if(isProductInWishlist(item.id)) { item.in_wishlist = true; } else { item.in_wishlist = false; } }); }
As we will store the product Ids into local storage the above function helps reading, updating, adding, and removing wishlist from localstorage, we will use “products_wishlist” as a key to store the product ids. In the above code the getProductsInWishlist() function retrieve the products stored in localstorge, if found we call JSON.parse() or null if not found.
addToWishlist() appends product Id into wishlist items so we check if the product already exist then we use localstorage.setItem(key, value) to store that item into localstorage.
likewise removeFromWishlist() filters the products in wishlist and remove this product id. And the clearWishlist() function empty the “products_wishlist” item entirely.
The other two functions isProductInWishlist() and updateWishlistProducts() used to check if particular product in wishlist and updating wishlist products “in_wishlist” flag respectively.
Now it’s time to update the components, so open ProductTemplateNormal.vue and update like so:
components/product-templates/ProductTemplateNormal.vue
<template> <div class="product-image-wrapper"> <div class="single-products"> <div class="productinfo text-center"> <img v-bind:src="item.gallery[0].image_url.medium" v-bind:alt="item.title" style="width: auto !important;" /> <h2>${{ item.price_after_discount }}</h2> <del v-if="item.is_discount_active">${{ item.price }}</del> <p>{{ item.title_short }}</p> <a v-if="!this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a> <a v-if="this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="removeFromCart(item.id)"><i class="fa fa-shopping-cart"></i>Remove from cart</a> </div> <div class="product-overlay"> <div class="overlay-content"> <h2>${{ item.price_after_discount }}</h2> <del v-if="item.is_discount_active">${{ item.price }}</del> <p>{{ item.title_short }}</p> <a v-if="!this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="addToCart(item.id)"><i class="fa fa-shopping-cart"></i>Add to cart</a> <a v-if="this.isProductAddedToCart(item.id)" href="javascript:void(0);" class="btn btn-default add-to-cart" @click.prevent="removeFromCart(item.id)"><i class="fa fa-shopping-cart"></i>Remove from cart</a> </div> </div> <div class="discount-ribbon" v-if="item.is_discount_active"><span>{{ item.discount }}%</span></div> </div> <div class="choose"> <ul class="nav nav-pills nav-justified"> <li v-if="!item.in_wishlist"><a href="javascript:void(0);" @click.prevent="addToWishList(item)"><i class="fa fa-plus-square"></i>Add to wishlist</a></li> <li v-if="item.in_wishlist"><a href="javascript:void(0);" @click.prevent="removeFromWishlist(item)"><i class="fa fa-trash-o"></i>Erase from wishlist</a></li> <li><nuxt-link :to="'/p/' + item.id + '/' + item.slug"><i class="fa fa-eye"></i>view item</nuxt-link></li> </ul> </div> </div> </template> <script> import {addToCart, removeFromCartByProductId, isProductInCart} from '../../helpers/cart'; import {addToWishlist, removeFromWishlist} from '../../helpers/wishlist'; export default { name: "ProductTemplateNormal", props: ["item"], data() { return { } }, methods: { addToCart(productId) { addToCart(productId, 1, this.$store, this.$router); }, isProductAddedToCart(productId) { return isProductInCart(productId, this.$store); }, removeFromCart(productId) { removeFromCartByProductId(productId, this.$store, this.$router); }, addToWishList(product) { addToWishlist(product.id); if(this.$router.currentRoute.name == 'index') { product.in_wishlist = true; } else { this.$store.commit('general/setWishedProduct', product.id); } }, removeFromWishlist(product) { removeFromWishlist(product.id); if(this.$router.currentRoute.name == 'index') { product.in_wishlist = false; } else { this.$store.commit('general/setUnwishedProduct', product.id); } } } } </script>
In this code i updated the add to wishlist button and added another button which removes from wishlist, if the user clicks in any button it will trigger the addToWishlist() or removeFromWishlist() methods. In addToWishlist() i invoked the helper function addToWishlist() we just created above to store the product Id into localstorage.
Next i checked if the current route is index which is the homepage then i set product.in_wishlist = true else if the current page not home which can be the shop/ page or category page then we should update the product via store because the shop/ page reads the products from store so i created two mututions in the general.js store which we will see them below.
The removeFromWishlist() works in the same way as addToWishlist().
Now let’s create the two mutations in general.js. Open store/general.js and add the these two mututions at the end of the mututions object:
setWishedProduct(state, productId) { const index = state.shop.products.data.findIndex(i => i.id == productId); state.shop.products.data[index].in_wishlist = true; }, setUnwishedProduct(state, productId) { const index = state.shop.products.data.findIndex(i => i.id == productId); state.shop.products.data[index].in_wishlist = false; }
After this update try to run the application and add or remove products into wishlist and inspect the browser localstorage.
In the same way update the product details page which has another button for adding and removing into wishlist.
pages/p/_id/_slug/index.vue
<template> <section> <div class="container"> <div class="row"> <div class="col-sm-12 padding-right" v-if="this.product"> <div class="product-details"><!--product-details--> <div class="col-sm-5"> <div v-if="this.product.gallery.length" id="similar-product" class="carousel slide" data-ride="carousel"> <div class="alert alert-success" v-if="this.$store.state.cart.success_message != ''">{{ this.$store.state.cart.success_message }}</div> <div class="alert alert-danger" v-if="this.$store.state.cart.error_message != ''">{{ this.$store.state.cart.error_message }}</div> <div class="sufee-alert alert with-close alert-danger alert-dismissible" v-if="this.$store.state.cart.validation_errors.length"> <ul v-for="(error, index) in this.$store.state.cart.validation_errors" :key="index"> <li>{{ error }}</li> </ul> </div> <!-- Wrapper for slides --> <div class="carousel-inner"> <div v-for="(imageItem, index) in this.product.gallery" :class="'item ' + (index == 0 ? 'active' : '')"> <a href=""><img :src="imageItem.image_url.product_gallery_preview" alt=""></a> </div> </div> <!-- Controls --> <a class="left item-control" href="#similar-product" data-slide="prev"> <i class="fa fa-angle-left"></i> </a> <a class="right item-control" href="#similar-product" data-slide="next"> <i class="fa fa-angle-right"></i> </a> </div> </div> <div class="col-sm-7"> <div class="product-information"><!--/product-information--> <div class="discount-ribbon-details" v-if="this.product.is_discount_active"><span>{{ this.product.discount }}%</span></div> <h2>{{ this.product.title }}</h2> <p v-if="this.product.product_code">Item Code: {{ this.product.product_code }}</p> <del v-if="this.product.is_discount_active" style="display: block">Price before: ${{ this.product.price }}</del> <span> <span>${{ this.product.price_after_discount }}</span> <span v-if="this.product.amount > 0 && !isProductAddedToCart(this.product.id)"> <label>Quantity:</label> <input type="text" v-model="cart_quantity" min="1" /> <button type="button" class="btn btn-fefault cart" @click="addToCart(product.id)"> <i class="fa fa-shopping-cart"></i> Add to cart </button> </span> <button type="button" class="btn btn-fefault cart" v-if="isProductAddedToCart(product.id)" @click="removeFromCart(product.id)"><i class="fa fa-trash-o"></i> Remove from cart</button> <button type="button" class="btn btn-default" title="add to wishlist" @click.prevent="addToWishList(product)" v-if="!product.in_wishlist"><i class="fa fa-heart-o"></i></button> <button type="button" class="btn btn-default" title="remove from wishlist" @click.prevent="removeFromWishlist(product)" v-if="product.in_wishlist"><i class="fa fa-heart"></i></button> </span> <p><b>Availability:</b> {{ this.product.amount > 0 ? 'In Stock (' + this.product.amount + ' items available)' : 'Not Available' }}</p> <p v-if="this.product.brand"><b>Brand:</b> {{ this.product.brand.title }}</p> <a href=""><img src="/images/product-details/share.png" class="share img-responsive" alt="" /></a> </div><!--/product-information--> </div> </div><!--/product-details--> <div class="category-tab shop-details-tab"><!--category-tab--> <div class="col-sm-12"> <ul class="nav nav-tabs"> <li class="active" v-if="this.product.description"><a href="#details" data-toggle="tab">Description</a></li> <li :class="(this.product.description ? '' : 'active')" v-if="this.features.length"><a href="#specifications" data-toggle="tab">product specifications</a></li> </ul> </div> <div class="tab-content"> <div class="tab-pane fade active in" id="details" v-if="this.product.description"> <div class="tab-body" v-html="this.product.description"> </div> </div> <div :class="'tab-pane fade ' + (this.product.description ? '' : 'active in')" v-if="this.features.length" id="specifications" > <div class="tab-body"> <ul class="specifications"> <li v-for="feature in this.features" v-bind:key="feature.id" :title="feature.value"> <strong>{{ feature.title }}:</strong> <span v-if="feature.type === 1"> {{feature.value}}</span> <span v-else :style="'background-color: ' + feature.value" class="color-box"></span> </li> </ul> </div> </div> </div> </div><!--/category-tab--> <div class="recommended_items" v-if="similarProducts.length"> <h2 class="title text-center">similar items</h2> <div id="recommended-item-carousel" class="carousel slide" data-ride="carousel"> <div class="carousel-inner"> <div v-for="(item, index) in this.similarProducts" :key="item.id" :class="'item ' + (index == 0 ? 'active' : '') "> <div class="col-sm-4" v-for="product in item.products" :key="product.id"> <ProductTemplateMini :item="product"></ProductTemplateMini> </div> </div> </div> <a class="left recommended-item-control" href="#recommended-item-carousel" data-slide="prev"> <i class="fa fa-angle-left"></i> </a> <a class="right recommended-item-control" href="#recommended-item-carousel" data-slide="next"> <i class="fa fa-angle-right"></i> </a> </div> </div> </div> </div> </div> </section> </template> <script> import {ProductApi} from '../../../../api/product'; import {ShopApi} from '../../../../api/shop'; import ProductTemplateMini from "../../../../components/product-templates/ProductTemplateMini"; import {addToCart, removeFromCartByProductId, isProductInCart} from '../../../../helpers/cart'; import {addToWishlist, isProductInWishlist, removeFromWishlist} from "../../../../helpers/wishlist"; export default { name: "ProductDetails", components: {ProductTemplateMini}, validate({params}) { return /^\d+$/.test(params.id); }, data() { return { similarProducts: [], cart_quantity: 1 } }, head() { return { title: 'Online Shop | ' + this.product.category.title + ' | ' + this.product.title, meta: [ { hid: 'description', name: 'description', content: this.product.title } ] } }, async asyncData(context) { const features = []; const response = await ProductApi.getProduct(context.app.$axios, context.params.id); let productFeatures = response.product.features; let categoryFeatures = response.product.category.features; if(productFeatures.length) { productFeatures.forEach((item) => { const featureDetail = categoryFeatures.find(f => f.id == item.field_id); features.push({ id: item.field_id, title: featureDetail.field_title, value: item.field_value, type: featureDetail.field_type }); }); } return { product: response.product, features: features } }, mounted() { setTimeout(() => { // re-initialize carousal if($("#similar-product").length) { $("#similar-product").carousel(); } // load similar products ShopApi.search(this.$axios, "category_id=" + this.product.category_id + "&except=" + this.product.id).then(res => { const productsData = res.products.data; if(productsData.length == 0) { this.similarProducts = []; } else { const totalProducts = productsData.length; const numCarousalItems = Math.ceil(totalProducts / 3); for(let i = 0; i < numCarousalItems; i++) { this.similarProducts.push({ id: i + '-' + i + '-' + i, products: [] }); } for(let i = 0; i < productsData.length; i++) { const itemIndex = parseInt(i / 3); if(this.similarProducts[itemIndex].products.length == 3) { this.similarProducts[itemIndex + 1].products.push(productsData[i]); } else { this.similarProducts[itemIndex].products.push(productsData[i]); } } } // re-initialize the boostrap carousal $("#recommended-item-carousel").carousel(); }); }, 200); }, methods: { addToCart(productId) { if(isNaN(parseInt(this.cart_quantity))) { alert("Please enter an integer value"); return; } if(parseInt(this.cart_quantity) <= 0) { alert("Please enter a value greater than or equal 1"); return; } addToCart(productId, parseInt(this.cart_quantity), this.$store, this.$router); }, isProductAddedToCart(productId) { return isProductInCart(productId, this.$store); }, removeFromCart(productId) { removeFromCartByProductId(productId, this.$store, this.$router); }, addToWishList(product) { addToWishlist(product.id); this.product.in_wishlist = true; }, removeFromWishlist(product) { removeFromWishlist(product.id); this.product.in_wishlist = false; } } } </script> <style scoped> #similar-product .carousel-inner .item img { margin-left: 26% !important; } .tab-pane { border: 1px solid #FE980F; } .tab-body { color: #000; background: #fff; margin-top: 4px !important; padding-left: 11px; } .tab-body ul { background: #fff !important; list-style: none outside none; margin-top: 0 !important; border-bottom: none !important; } .tab-body ul.specifications { list-style: auto !important; } .tab-body ul.specifications li { list-style-type: disc !important; padding-bottom: 6px; } .color-box { width: 16px; height: 16px; display: inline-block; vertical-align: middle; } </style>
In this code all i have done is adding two other buttons beside the button that add to the cart. Then i created two methods attached the click events to each button which set this.product.in_wishlist = true or this.product.in_wishlist = false.
Now the adding and removing works fine in the two pages but there is one issue which if the user reload the page then the buttons we be reset, so how we can fix this?.
Fixing The Reload Issue
To overcome this problem and set the product that are already in the wishlist we need to make some updates.
First open pages/index.vue and update it like so:
pages/index.vue
<template> <div> <Slider v-if="sliderProducts.length" :sliderProducts="sliderProducts"></Slider> <section> <div class="container"> <div class="row"> <div class="col-sm-12 padding-right"> <LatestItems v-if="latestProducts.length" :latestProducts="latestProducts"></LatestItems> <FeaturedCategories v-if="featuredCategories.length" :featured-categories="featuredCategories"></FeaturedCategories> <FeaturedProducts v-if="featuredItems.length" :featuredItems="featuredItems"></FeaturedProducts> </div> </div> </div> </section> </div> </template> <script> import {HomeApis} from '../api/home'; import Slider from '../components/home-components/Slider'; import LatestItems from "../components/home-components/LatestItems"; import FeaturedCategories from "../components/home-components/FeaturedCategories"; import FeaturedProducts from "../components/home-components/FeaturedProducts"; import {updateWishlistProducts} from '../helpers/wishlist'; export default { components: { FeaturedProducts, FeaturedCategories, LatestItems, Slider }, data() { return { sliderProducts: [], latestProducts: [], featuredCategories: [], featuredItems: [] } }, head() { return { title: 'Online Shop | Home', meta: [ { hid: 'description', name: 'description', content: 'Online Shop Home Page' } ] } }, mounted() { // retrieve slider products HomeApis.getSliderProducts(this.$axios).then(res => { this.sliderProducts = res.products; }); // retrieve latest items HomeApis.getLatestProducts(this.$axios).then(res => { this.latestProducts = res.products; updateWishlistProducts(this.latestProducts); }); // featured categories HomeApis.getFeaturedCategories(this.$axios).then(res => { this.featuredCategories = res.categories; this.featuredCategories.map(category => { category.products = category.products.slice(0, 4); }); }); // featured products // Try to reduce the main products array into sub arrays, each array with 3 products in order for // the bootstrap carousal to render them properly HomeApis.getFeaturedProducts(this.$axios).then(res => { if(res.products.length == 0) { this.featuredItems = []; } else { const totalProducts = res.products.length; const numCarousalItems = Math.ceil(totalProducts / 3); for(let i = 0; i < numCarousalItems; i++) { this.featuredItems.push({ id: i + '-' + i + '-' + i, products: [] }); } for(let i = 0; i < res.products.length; i++) { const itemIndex = parseInt(i / 3); if(this.featuredItems[itemIndex].products.length == 3) { this.featuredItems[itemIndex + 1].products.push(res.products[i]); } else { this.featuredItems[itemIndex].products.push(res.products[i]); } } } }); } } </script>
The only update i made above is importing the updateWishlistProducts() function and calling it inside HomeApis.getLatestProducts(). updateWishlistProducts() loops over all products and checks if any product in wishlist or not.
Second update pages/p/_id/_slug/index.vue add this snippet at the end of the mounted() hook:
// set wishlist products setTimeout(() => { if (isProductInWishlist(this.product.id)) { this.product.in_wishlist = true; } }, 600);
Third update the store/general.js store, add this import at the top of the file:
import {updateWishlistProducts} from "../helpers/wishlist";
Then update fetchShopProducts() action like so:
async fetchShopProducts({commit, state}) { let searchParams = []; if(state.shop.page && parseInt(state.shop.page) >= 1 ) { searchParams.push("page=" + parseInt(state.shop.page)); } if(state.shop_filter.categoryId && parseInt(state.shop_filter.categoryId) > 0) { searchParams.push("category_id=" + parseInt(state.shop_filter.categoryId)); } if(state.shop_filter.brand_id && parseInt(state.shop_filter.brand_id) > 0) { searchParams.push("brand_id=" + parseInt(state.shop_filter.brand_id)); } if(state.shop_filter.from_price) { searchParams.push("from_price=" + state.shop_filter.from_price); } if(state.shop_filter.to_price) { searchParams.push("to_price=" + state.shop_filter.to_price); } if(state.shop_filter.keyword) { searchParams.push("keyword=" + state.shop_filter.keyword); } const response = await ShopApi.search(this.$axios, searchParams.join("&")); updateWishlistProducts(response.products.data); commit('setProducts', response.products); }
Running the application again you will see the reload problem is fixed.
Displaying Wishlist Products
Open pages/wishlist.vue and update with this code:
<template> <div class="container"> <div class="bg"> <div class="row"> <div class="col-sm-12"> <h2 class="title text-center">My Wishlist</h2> </div> </div> <div class="row"> <div class="col-sm-12"> <div class="table-responsive"> <table class="table table-cart"> <tbody v-if="this.products.length === 0"> <tr> <td colspan="5" class="text-center">You have no items in your wishlist!</td> </tr> </tbody> <tbody v-if="this.products.length > 0"> <tr class="cart-item" v-for="item in this.products" :key="item.id"> <td class="product-remove"> <a href="#" @click.prevent="removeFromWishlist(item.id)"> <i class="fa fa-trash-o"></i> </a> </td> <td class="product-thumbnail"> <nuxt-link :to="'/p/' + item.id + '/' + item.slug"><img :src="item.gallery[0].image_url.small" v-bind:alt="item.title"></nuxt-link> </td> <td class="product-name"> <nuxt-link :to="'/p/' + item.id + '/' + item.slug">{{ item.title }}</nuxt-link> </td> <td class="product-price">$<span>{{ item.price_after_discount }}</span></td> <td class="product-stock"> <span>{{ item.amount > 0 ? 'In Stock' : 'Out Of Stock' }}</span> </td> </tr> </tbody> </table> </div> <button class="btn btn-danger pull-right" style="margin: 10px" v-if="this.products.length > 0" @click="clearWishlist()">Clear Wishlist</button> </div> </div> </div> </div> </template> <script> import {clearWishlist, getProductsInWishlist, removeFromWishlist} from "../helpers/wishlist"; export default { name: "wishlist", data() { return { products: [] } }, mounted() { this.getProducts(); }, methods: { getProducts() { if(getProductsInWishlist() != null && getProductsInWishlist().length > 0) { this.$axios.get("/api/product/products-by-ids?ids=" + getProductsInWishlist()).then(response => { this.products = response.data.products; }); } else { this.products = []; } }, removeFromWishlist(productId) { removeFromWishlist(productId); this.getProducts(); }, clearWishlist() { if(confirm("Are you sure?")) { clearWishlist(); this.products = []; } } } } </script> <style scoped> .table-cart.table>tbody>tr { border-top: 1px dashed #ddd; text-align: left; } .table-cart.table>tbody>tr:first-child { border-top: none; } .table-cart .product-remove { display: table-cell; vertical-align: middle; height: 100%; font-size: 1.1rem; } .table-cart td:first-child { width: 50px; padding: 0; } .table-cart.table>tbody>tr:first-child>td { border-top: none; } .table-cart img { height: 100%; max-height: 100px; } .table-cart .product-name { font-weight: 600; } .product-remove i { font-size: 19px; } </style>
This template is so simple, i fetch the product ids using getProductsInWishlist() function and then using those product ids list i send an http request to another lumen api which we will create below ‘/api/product/products-by-ids‘ which retrieves products details by ids. In the template i looped over the products and display each product image, title, amount and a remove icon. In the bottom of the products grid there is a button to clear the wishlist which when clicked triggers the clearWishlist() helper function.
Now let’s create the new api in the lumen project, open the lumen project and open app/Traits/SearchApi.php and add this method to the end of the class:
public function getProductsByIds($ids) { $products = Product::with('gallery', 'category', 'brand') ->select('id', 'title', 'description', 'price', 'discount', 'discount_start_date', 'discount_end_date', 'category_id', 'brand_id', 'featured', 'amount') ->has('gallery'); $products->whereIn("id", $ids); return response()->json(['products' => $products->get()]); }
Next update the ProductsController.php add this method to the end of the class:
public function productsByIds(Request $request) { return $this->getProductsByIds(explode(",", $request->input("ids"))); }
Finally update routes/web.php in the product route group that is not require authentication
$router->group(['prefix' => 'product'], function () use ($router) { $router->get('/', 'ProductsController@index'); $router->get('/slider-products', 'ProductsController@sliderProducts'); $router->get('/latest-products', 'ProductsController@latestProducts'); $router->get('/featured-products', 'ProductsController@featuredProducts'); $router->get('/search-products', 'ProductsController@searchProducts'); $router->get('/products-by-ids', 'ProductsController@productsByIds'); $router->get('/{id}', 'ProductsController@show'); });
Fine! you can check now the wishlist page.
Continue To Part 23: Contact Form