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"> © {{ 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
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!