Laravel Ecommerce Tutorial: Part 11, The Homepage

Laravel Ecommerce Tutorial: Part 11, The Homepage

Given Ncube

Up until this point we've been building the admin side of the ecommerce site, the backend side where the magic happens.

Now that the site owners have the ability to add and manage products, let's allow them to sell and make money. In this post we will create the homepage for the site.

For the frontend we have options, I've been using bootstrap in the backend and will continue to use it on this post because well, components what not

So basically we want to show a hero of sorts probably randomized products show cases, some information about the site, a carousel of featured products, one featured product with the ability to buy now well pretty much the basic stuff you expect to see on the home page.

Notice if you go to the index of our website you get a 404, let's register a controller and route to show the homepage.

Start by generating a controller

php artisan make:contoller HomeController --test

Next, generate the view we will be using with this controller

php artisan make:view layouts.home
php artisan make:view home.index -e layouts.home

Next we register the route for our home. We will use a separate route group group since we will be different middleware than the admin groups. In the routes/web.php file, add the following snippet below the admin routes group

use App\Http\Controllers\HomeController as FrontendHomeController;

Route::group([], static function () {
    Route::get('/', FrontendHomeController::class, 'index')->name('home.index');
});

Now go to App\Http\Controllers\HomeController and define the index action

/**
 * Display the application home page.
 *
 * @return Renderable
 */
public function index(): Renderable
{
    return view('home.index');
}

By now if you visit the index url you will just see a blank page

The layout

For our theme we will be using an earlier version of the Bootstrap Ecommerce theme based on Bootstrap 4, it's no longer availble on the website, it was open source, closed and recently owned by MDB.

Anyways, I still have a copy of the stylesheets compressed in a zip file you can download from this link

Download the file and extract the contents in your resources/sass folder

Create another file _store-variables.scss so we can customize bootstrap, or simply download it from this link

And our styles are now let's add some javascript. Create a file resources/js/store.js and the following code to register turbo and stimulus

import './bootstrap';
import './elements/turbo-echo-stream-tag';
import './libs/turbo';
import './libs';

Finally, let's tell vite to compile our newly created files. Open vite.config.js add append the following snippet to input array like this

export default defineConfig({
    plugins: [
        laravel({
            input: [
                'resources/sass/app.scss',
                'resources/js/app.js',
                'resources/js/store.js',
                'resources/sass/store.scss',
            ],
            refresh: true,
        }),
    ],
});

Our setup is done, now, let's create the layout. Open the layouts.home view and add the following html snippet

<!DOCTYPE HTML>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <meta name="csrf-token" content="{{ csrf_token() }}" />

    <title>@yield('title') | {{ config('app.name') }}</title>

    @vite(['resources/sass/store.scss', 'resources/js/store.js'])
</head>

<body>
<h1>Hello world</h1>
</body>

Save the file and visit the home page to confirm if your styling was setup correctly.

Mine was, so let's now add the actual layout for the home page, replace our "hello world" with

    <header class="section-header border-bottom">

        <section class="header-main border-bottom">
            <div class="container">
                <div class="row d-flex align-items-center justify-content-between">
                    <div class="col-2 navbar-light d-md-none">
                        <button class="navbar-toggler border-0" type="button" data-toggle="collapse"
                                data-target="#main_nav" aria-controls="main_nav" aria-expanded="false"
                                aria-label="Toggle navigation">
                            <span class="navbar-toggler-icon text-dark"></span>
                        </button>
                    </div>
                    <div class="col-md-3 col-4">
                        <div class="brand-wrap mx-auto">
                            <a href="{{ route('home.index') }}">
                                <img class="logo" src="{{ asset('images/JPEG/Apple Plug Logo 1.png') }}">
                            </a>
                        </div> <!-- brand-wrap.// -->
                    </div>
                    <div class="col-lg-6 col-sm-12 col-5 d-none d-md-block">
                        <form action="#" class="search" {{ stimulus_controller('form-search') }}>
                            <div class="input-group w-100">
                                <input type="text" class="form-control" placeholder="Search" {{ stimulus_target('form-search', 'input') }} {{ stimulus_action('form-search', 'searchByEnter', 'keypress') }}>
                                <div class="input-group-append">
                                    <button class="btn btn-primary" type="button" {{ stimulus_action('form-search', 'search') }}>
                                        <i class="fa fa-search"></i>
                                    </button>
                                </div>
                            </div>
                        </form>
                    </div> <!-- col.// -->
                    <div class="col-md-2 col-4">
                        <div class="widgets-wrap float-right ml-auto mt-0">
                            <div class="widget-header mr-3 ">
                                <a href="#" class="icon icon-sm rounded-circle border">
                                    <i class="fas fa-shopping-cart"></i>
                                </a>
                                <span class="badge badge-pill badge-danger notify">1</span>
                            </div>
                        </div> <!-- widgets-wrap.// -->
                    </div> <!-- col.// -->
                </div> <!-- row.// -->
            </div> <!-- container.// -->
        </section> <!-- header-main .// -->

        <nav class="navbar navbar-main navbar-expand-lg navbar-light mb-0 py-0 pb-y-2 mb-md-2">
            <div class="container">
                <div class="collapse navbar-collapse" id="main_nav">
                    <ul class="navbar-nav">
                        <li class="nav-item dropdown">
                            <a class="nav-link" href="{{ route('home.index') }}">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Shop</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="#">Contact</a>
                        </li>
                        @foreach ($categories->take(4) as $category)
                            <li class="nav-item">
                                <a class="nav-link"
                                   href="#">{{ $category->name }}
                                </a>
                            </li>
                        @endforeach
                        <li class="nav-item d-md-none">
                            <div class="col-12 py-5">
                                <form action="#" class="search" {{ stimulus_controller('form-search') }}>
                                    <div class="input-group w-100">
                                        <input type="text" class="form-control" placeholder="Search" {{ stimulus_target('form-search', 'input') }} {{ stimulus_action('form-search', 'searchByEnter', 'keypress') }}>
                                        <div class="input-group-append">
                                            <button class="btn btn-primary" type="button" {{ stimulus_action('form-search', 'search') }}>
                                                <i class="fa fa-search"></i>
                                            </button>
                                        </div>
                                    </div>
                                </form>
                            </div>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>

Change the logo image to your own logo or simply download this one from this link

Notice the categories loop that displays categories on the navbar, we can't keep including the categorieis variable on every controller but instead we will use a view composer,

Actually we will be using quite a number of them throughtout this tutorial series.

First create a file called Categories.php in app/View/Composers and paste the following code

namespace App\View\Composers;

use App\Models\Category;
use Illuminate\View\View;

class Categories
{
    /**
     * Bind data to the view.
     *
     * @param View $view
     * @return void
     */
    public function compose(View $view): void
    {
        $categories = Category::with('children')
            ->whereNull('parent_id')
            ->withCount('products')
            ->get()
            ->filter(fn($category) => $category->products_count > 0);

        $view->with('categories', $categories);
    }
}

A view composer allows us to bind a piece of data to a specific so it's included each time a view is rendered. In our case we query all categories and filter out those without products in them because, well, what's point.

For the composer to actually work we need to register it, normally in a service provider. Let's create a provider for this

php artisan make:provider ViewServiceProvider

After the provider is created, register it to the providers array in config/app.php

Now open the provider and register the composer in the boot method

/**
 * Bootstrap services.
 *
 * @return void
 */
public function boot()
{
    View::composer('layouts.home', Categories::class);
}

Now if you revisit the index page you should see a nice header with nav and search

Next let's add the footer yield the content. Paste the following snippet after the closing header tag

<footer class="section-footer border-top padding-y">
    <div class="container">
        <section class="footer-top padding-y">
            <div class="row">
                <aside class="col-md">
                    <h6 class="title">Company</h6>
                    <ul class="list-unstyled">
                        <li> <a href="#">Home</a></li>
                        <li> <a href="#">About us</a></li>
                        <li> <a href="#">Shop</a></li>
                        <li> <a href="#">Contact us</a></li>
                    </ul>
                </aside>
                <aside class="col-md">
                    <h6 class="title">Legal Stuff</h6>
                    <ul class="list-unstyled">
                        <li> <a href="#">Refund Policy</a></li>
                        <li> <a href="#">Terms of Service</a></li>
                        <li> <a href="#">Privacy Policy</a></li>
                        <li> <a href="#">Open dispute</a></li>
                    </ul>
                </aside>
                <aside class="col-md">
                    <h6 class="title">Account</h6>
                    <ul class="list-unstyled">
                        <li> <a href="#"> User Login </a></li>
                        <li> <a href="{{ route('register') }}"> User register </a></li>
                        <li> <a href="#"> My Account </a></li>
                        <li> <a href="#"> My Orders </a></li>
                    </ul>
                </aside>
                <aside class="col-md">
                    <h6 class="title">Social</h6>
                    <ul class="list-unstyled">
                        <li><a href="#"> <i class="fab fa-facebook"></i>
                                Facebook </a></li>
                        <li><a href="#"> <i class="fab fa-twitter"></i>
                                Twitter </a></li>
                        <li><a href="#"> <i class="fab fa-instagram"></i>
                                Instagram </a></li>
                        <li><a href="#"> <i class="fab fa-whatsapp"></i>
                                WhatsApp </a></li>
                    </ul>
                </aside>
            </div> <!-- row.// -->
        </section> <!-- footer-top.// -->

        <section class="footer-bottom border-top row">
            <div class="col-md-2">
                <p class="text-muted"> &copy {{ date('Y') }} {{ config('settings.general.legal_name') }} </p>
            </div>
            <div class="col-md-8 text-md-center">
                <span class="px-2">{{ config('settings.general.contact_email') }}</span>
                <span class="px-2">{{ config('settings.general.phone') }}</span>
            </div>
            <div class="col-md-2 text-md-right text-muted">
                <i class="fab fa-lg fa-cc-visa"></i>
                <i class="fab fa-lg fa-cc-paypal"></i>
                <i class="fab fa-lg fa-cc-mastercard"></i>
            </div>
        </section>
    </div>
</footer>

If you save and refresh your page it should look something like the image below

ecommece layout without content

Now let's move to the actual content of this post

The hero

Every ecommerce this above the fold hero thing and that's what we will implement below, open the home.index view and paste the following snippet

<section class="section-intro bg-primary p-0">
        <div id="carousel1_indicator" class="carousel slide" data-ride="carousel" style="height: 80vh">
            <ol class="carousel-indicators">
                <li data-target="#carousel1_indicator" data-slide-to="0" class="active"></li>
            </ol>
            <div class="carousel-inner">
                <div class="carousel-item active" style="height: 80vh">
                    <div class="d-block w-100 h-100 text-white "
                         style="background-size: cover; background-repeat: no-repeat; background-position: center; background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.6)), url('{{ asset('images/banners/7.jpg') }}')">
                        <div class="container h-100">
                            <div class="d-flex h-100 align-items-center">
                                <div class="col-md-7 text-center text-md-start">
                                    <h1 class="text-capitalize display-4" style="font-weight: 500">
                                        quality guaranteed products at reasonable pricing
                                    </h1>
                                    <p class="lead">
                                        We bring you amazing products of your choice backed by exceptional after care service.
                                    </p>
                                    <a href="#" class="btn btn-primary">
                                        Shop Now
                                    </a>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </section>

Download the banners folder from this link tot get the background image, or you can just find another on Unsplash.

Add the following snippet to show the "why you should buy from us" section

<section class="section-specials padding-y border-bottom">
    <div class="container">
        <div class="row">
            <div class="col-md-4 mb-4 mb-md-0">
                <figure class="itemside">
                    <div class="aside">
                        <span class="icon-sm rounded-circle bg-primary">
                            <i class="fa fa-money-bill-alt white"></i>
                        </span>
                    </div>
                    <figcaption class="info">
                        <h6 class="title">Reasonable prices</h6>
                        <p class="text-muted">We bring you amazing products of your choice backed by exceptional after care service.</p>
                    </figcaption>
                </figure> <!-- iconbox // -->
            </div><!-- col // -->
            <div class="col-md-4  mb-4 mb-md-0">
                <figure class="itemside">
                    <div class="aside">
                        <span class="icon-sm rounded-circle bg-danger">
                            <i class="fa fa-comment-dots white"></i>
                        </span>
                    </div>
                    <figcaption class="info">
                        <h6 class="title">Customer support 24/7 </h6>
                        <p class="text-muted">Ring us anytime to get assistance on anything that might be bothering you when using any of our products  </p>
                    </figcaption>
                </figure> <!-- iconbox // -->
            </div><!-- col // -->
            <div class="col-md-4  mb-4 mb-md-0">
                <figure class="itemside">
                    <div class="aside">
                        <span class="icon-sm rounded-circle bg-success">
                            <i class="fa fa-truck white"></i>
                        </span>
                    </div>
                    <figcaption class="info">
                        <h6 class="title">Quick delivery</h6>
                        <p class="text-muted">
                            Enjoy the convinience of quick deliveries as soon as you place an order with us
                        </p>
                    </figcaption>
                </figure> <!-- iconbox // -->
            </div><!-- col // -->
        </div> <!-- row.// -->

    </div> <!-- container.// -->
</section>

Now let's randomly pick some categories to show on the home page you, know to boost sales and such

<section class="padding-y bg-light">
    <div class="container">
        <!-- ============ COMPONENT BS CARD WITH IMG ============ -->
        <div class="row gy-3">
            @foreach ($categories->take(3) as $category)
                <div class="col-lg-4 col-md-6 mb-4 mb-md-0">
                    <article class="card bg-dark border-0">
                        <img src="{{ $category->products->random()->fetchAllMedia()->random()?->file_url }}"
                             class="card-img opacity" style="object-fit: cover" height="200">
                        <div class="card-img-overlay">
                            <h5 class="mb-0 text-white">{{ $category->name }}</h5>
                            <p class="card-text text-white">{{ Str::limit($category->description) }}</p>
                            <a href="#"
                               class="btn btn-secondary">
                                Discover
                            </a>
                        </div>
                    </article>
                </div>
            @endforeach
        </div>
    </div>
</section>

To include the categories variable let's register a view composer to this view. Open the service provider and add this code to the boot method

 View::composer('home.index', Categories::class);

Now there's a slight problem with the blade snippet above, when we call fetchAllMedia() on a random product sometimes the product might not have an image and that may cause some unwanted side effects

We are going to overwrite the fetchAllMedia() method to return a default image when no image has been added to the product.

fetchAllMedia returns a collection of Media models from the cloudinary-laravel package, so we will create one of our own to return when nothing is found

create a class called App\Null\MediaAlly\DefaultMedia and define it like this

<?php

namespace App\Null\MediaAlly;

use Illuminate\Support\Facades\File;
use SplFileInfo;

class DefaultMedia
{
    public string $file_url;
    
    public int|false $size;
    public string $file_name;
    public string|false $file_type;
    
    public function __construct()
    {
        $file = new SplFileInfo(public_path('images/null/default.png'), );

        $this->file_url = asset('images/null/default.png');
        $this->file_type = $file->getType();
        $this->file_name = $file->getFilename();
        $this->size = $file->getSize();
    }
}

So this class creates an SplFileInfo object of a null image from that given path, make sure yours is not empty, you can download a null image from this link

Now open the Product model and replace the line use MediaAlly with

use MediaAlly {
    MediaAlly::fetchAllMedia as fetchMedia;
}

This is so we can safely overwrite fetchAllMedia()

Now add the following methods to overwrite this method

/**
 * Fetch all media that belong to this product
 *
 * @return Collection
 */
public function media(): Collection
{
    $media = $this->fetchMedia();

    if ($media->count() < 1) {
        return collect([new DefaultMedia]);
    }

    return $media;
}

/**
 * @see Product::media()
 *
 * @return Collection
 */
public function fetchAllMedia(): Collection
{
    return $this->media();
}

If the product doesn't have any images we return a collection of the default image else we return the images and that's a poor man's implementation of the null object pattern

As I was halfway through this tutorial I realized the home page is better created at last after other pages have been made so we can link properly without using placeholders.

With that said, we will end this tutorial and continue to build the products index page in the next post to allow customers to, well, shop

In the meantime make sure you follow me here on DEV so you can get a ping when the new post is up.

Happy coding!