Laravel Ecommerce Tutorial: Part 11, The Homepage
Tags: webdev, laravel, php
Published: 2023-08-24 19:31:50

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

```bash
php artisan make:contoller HomeController --test
```

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

```bash
php artisan make:view layouts.home
```

```bash
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

```php
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

```php
/**
 * 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](https://res.cloudinary.com/dwinzyahj/raw/upload/v1692545400/ui-assets_rywxtk.zip)

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](https://res.cloudinary.com/dwinzyahj/raw/upload/v1692725425/_store-variables_bdubuz.scss)

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

```javascript
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

```javascript
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

```html
<!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

```html
    <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](https://res.cloudinary.com/dwinzyahj/raw/upload/v1692546907/JPEG_jokttc.zip)

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,](https://laravel.com/docs/10.x/views#view-composers)

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

```php
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

```bash
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

```php
/**
 * 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

```html
<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](https://res.cloudinary.com/dwinzyahj/image/upload/v1692548209/Screenshot_2023-08-20_at_18-12-37_Laravel_ady9qu.png "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

```html
<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

```html
<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

```html
<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

```php
 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
<?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

```php
use MediaAlly {
    MediaAlly::fetchAllMedia as fetchMedia;
}
```

This is so we can safely overwrite `fetchAllMedia()`

Now add the following methods to overwrite this method

```php
/**
 * 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!