Laravel Vue Heroku full-stack application development with Ilori Stephen A. In today’s lesson, I will show you how to build a full stack application. I am a full-stack software developer with two years of experience in building, updating, debugging, and testing web applications. In this lesson, I will be teaching you how to build a Fullstack CRUD application using Laravel and Vuejs.

Before we begin, I’m sure you’d like us to get straight to the codes but why don’t we take some time off while I introduce you to some terms and the requirements for this lesson.

Laravel is a PHP Framework used in building sophisticated SASS applications while Vuejs is a javascript frontend framework for building reactive frontend applications or Single Page Applications.

Why Should You Learn How To Build A Fullstack Application

There are so many reasons to build a Fullstack Application, but because of time, we will only touch a few points.

  1. Building a Fullstack Application exposes one to a lot of development experience which actually makes you a better software developer. I mean who doesn’t wanna get better?
  2. When you build a Fullstack Application, you have all your codes in one place and this makes updating your code easier as it’s within reach. Unlike other applications where you have the Client(Frontend) separated from the server(Backend) writing updates for them can really be a huge pain.
  3. Another cool reason why you should learn how to build a full-stack application is that it gives you the edge over other developers as you have been able to gather more experience by working on both Frontend and Backend channels in the application development process.

With that being said, I’m sure you are already thinking about becoming a Fullstack Developer.

Glossary

While I was building this lecture, I came across several Tech Jargons related to this lecture. So in order for you not to get lost when you come across some of the terms, I have decided to share them with you.

  1. Migrations: When you hear the word migration, you start thinking of herds migrating or something but that’s not the case really. For this lesson, A migration is a service that allows you to create a table in your database without actually using your database manager.
  2. Scaffolding: Well, we are not talking about the physical building, but a Scaffolding aims to quickly help you get started in your project or app by creating a skeleton or boilerplate for you. Whatever you are comfortable with.
  3. Endpoints: Simply put, an Endpoint is one End of a communication channel. When an API interacts with another system, The touchpoints of this communication are called Endpoints. You can read more on building REST API here.
  4. Model: In Laravel, A Model is a class that represents the logical structure and relationship of underlying data tables.

Those are the few terms I was able to research but as we proceed, I’ll do my best to explain any term that I feel you might find difficult while reading.

Project Requirements

In order for us to get the best out of this lecture, there are some requirements that need to be satisfied. So I’d like us to go through the requirements before we get to the codes.

  1. Experience: In order to get the best out of this course, I recommend that you have more than basic PHP programming experience.
  2. PHP: It is also important that you have the latest version of PHP installed. If that’s not possible, I recommend having at least PHP versions from 7.
  3. Laravel: After we have satisfied the requirements above, we can go ahead and install Laravel. Head to their official website to learn how to install the Framework.
  4. NPM: For this lesson, I also recommend that you have Nodejs and NPM installed. This is because we will have to install some Nodejs packages for development purposes. Head over to the official Nodejs website to learn how to install the Framework.
  5. Passport: For this lesson, I’d advise you to understand how to set up passport for authentication in Laravel. I won’t be covering this because of time. You can visit laravel’s Official Website or check out this resource on Medium. In the coming weeks, I’d also like to talk about how to get started with passport on your laravel application with you.
  6. Heroku Account: I also advise that you create an Heroku Account if you don’t have one. I won’t talk about this as well because of the time.
  7. Text Editor Or IDE: It is also important that you have a text editor or IDE to write out the codes listed in this lesson. There are a whole lot of text editors out there such as Bracket, Atom, & Visual Studio Code.

With the requirements all satisfied, we can now start writing some codes.

1. Setting The Application Environment

Under this section, we will talk about how to set our application configuration which in this lecture is just about our Database.

The .env File (Root Directory)

The first thing we will be editing in this project is the .env which exists in our project’s root directory. This file contains all the credentials needed for our application to run.

For this lesson, we are editing the database section of the config alone. You can modify the line in the github gist to fit your database credentials.

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=ld_talk_db
DB_USERNAME=root
DB_PASSWORD=
view raw .env hosted with ❤ by GitHub

The Database.php File (Config Folder)

Just inside of the config folder is a database.php file. Inside this file, we will find all the functions needed to establish a database connection depending on the database driver.

Inside of the file, we will be editing the line that reads mysql since we are using mysql as our database. If you are stuck, I have attached a github gist below for you to get familiar with the line you have to edit with the same values we used when changing the database values in our .env file.

<?php
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'ld_talk_db'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
?>
view raw database.php hosted with ❤ by GitHub

With that out of the way, we have successfully set up our database connection for our application. The next thing we have to do is create Models, Migrations & Seeds For Our Application.

2. Creating The Models, Migrations & Database Seeds

When building applications, we always need some data we can work with or manipulate at least. The creators of Laravel understood this so they made it very easy to create mysql tables without visiting phpmyadmin to create the tables manually.

They went extra and made some seeder functions that would help fill the created tables with some dummy data that we can test with.

The Models

Under this section, we will talk about how to create Models with Laravel. Luckily for us, Laravel has a whole bunch of amazing commands that we can run from the cli to get started.

With that said, let’s open up our project’s root directory in a new terminal window or command prompt and enter the command below.

php artisan make:model -m Comment
php artisan make:model -m Forum
view raw artisan.sh hosted with ❤ by GitHub

By using the command above, we were able to create four files. We have two files created in the app directory and two files created in the migrations folder inside the database directory.

Editing The Models File

We will start by editing the two new files created inside of the app folder root. It is also important to note that we have a User Model inside of the same app folder root.

The Comment.php Model/File is an alias to the comments table in our created database. Inside of it, we can define relationships and also columns related to the comments table. With that said replace the content of your Comment.php file with the github gist below. There will be some explanation to the codes that were added to the file.

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $fillable = ['id', 'user_id', 'forum_id', 'comment'];
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
}
view raw Comment.php hosted with ❤ by GitHub

The Forum.php Model/File is also an alias to the forums table in our applications mysql database. Inside of this file, we can define relationships, columns, and a few more sophisticated features as well. Now that we understand what a Laravel Model is all about, replace the content of your Forum.php file with the code below. I will explain the additional codes in the file later.

<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Forum extends Model
{
protected $fillable = ['id', 'user_id', 'title', 'content', 'likes', 'comments', 'created_at', 'updated_at'];
public function commentModel()
{
return $this->hasMany('App\Comment', 'forum_id');
}
public function user()
{
return $this->belongsTo('App\User');
}
}
view raw Forum.php hosted with ❤ by GitHub

By default, Laravel creates a User.php Model for us. I’m not going to go over what a model is at this juncture but I’ll advise that you update the content of that User.php file with this instead.

<?php
namespace App;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasApiTokens, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function forumModel()
{
return $this->hasMany('App\Forum', 'user_id');
}
public function comments()
{
return $this->hasMany('App\Comment', 'user_id');
}
}
view raw User.php hosted with ❤ by GitHub

The Migrations

While we were creating our Application Models with the php artisan make:model -m Command, Laravel took care of our Migrations for us thanks to the -m alias we added to our command.

I already explained what migration is at the beginning of this lesson. So, we will get straight to updating the content of the created migration files.

By default, Laravel comes with around 2-3 migration files by default. But for this lesson, we will only be editing 2 of the files inside of this migration folder.

  1. Updating 2020_09_10_084130_create_comments_table Migration File: This file is responsible for creating our comments table as its name implies. It also contains a few methods from the Blueprint class passed to the Schema::create callback function. The methods are similar to defining our database columns datatype.

    I’m sure the definition of a Migration is very clear now. Now that we are on the same page, we can replace exchange the content of the comments migration file with the code below.

    <?php
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    class CreateForumsTable extends Migration
    {
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
    Schema::create('forums', function (Blueprint $table) {
    $table->id();
    $table->bigInteger('user_id')->unsigned();
    $table->text('title');
    $table->text('content');
    $table->bigInteger('likes');
    $table->bigInteger('comments');
    $table->timestamps();
    });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
    Schema::dropIfExists('forums');
    }
    }

  2. Updating 2020_09_11_102345_create_forums_table.php Migration File: This File helps us create a forum mysql table in our application’s database. I’m pretty sure we are on the same page with the definition of a migration. Let’s get to the next code exchange by replacing the forums migration file with the git gist below.

    <?php
    use Illuminate\Database\Migrations\Migration;
    use Illuminate\Database\Schema\Blueprint;
    use Illuminate\Support\Facades\Schema;
    class CreateForumsTable extends Migration
    {
    /**
    * Run the migrations.
    *
    * @return void
    */
    public function up()
    {
    Schema::create('forums', function (Blueprint $table) {
    $table->id();
    $table->bigInteger('user_id')->unsigned();
    $table->text('title');
    $table->text('content');
    $table->bigInteger('likes');
    $table->bigInteger('comments');
    $table->timestamps();
    });
    }
    /**
    * Reverse the migrations.
    *
    * @return void
    */
    public function down()
    {
    Schema::dropIfExists('forums');
    }
    }

We are getting pretty close to creating controllers and views for our application but before we get to that, we have a few more things to do. One of which is creating database seeds.

Creating Database Seeds

Laravel is has a lot of awesome packages that makes development a breeze. One of these packages is seeding or filling up our database with dummy data for testing/development. Let’s create some dummy forums and some dummy users for our application.

The Forums Table Seeder

Copy the code snippet below the line by line and execute it by opening up the project root directory in your computer terminal or command prompt.

php artisan make:seeder ForumsTableSeeder
php artisan make:seeder UsersTableSeeder
php artisan make:factory ForumFactory
view raw Forumsseeder.sh hosted with ❤ by GitHub

You’ll find that after entering those command, two seeder files ForumsTableSeeder.php and UsersTableSeeder.php was created inside of the seeds folder. We will get to the third file created later.

Without saying much, I’d like us to replace the content of the ForumsTableSeeder.php file with the code below.

<?php
use Illuminate\Database\Seeder;
class ForumsTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
factory(App\Forum::class, 5)->create(['user_id' => 1]);
factory(App\Forum::class, 5)->create(['user_id' => 2]);
factory(App\Forum::class, 5)->create(['user_id' => 3]);
factory(App\Forum::class, 5)->create(['user_id' => 4]);
factory(App\Forum::class, 5)->create(['user_id' => 5]);
}
}

We have a method that is fired by our application when we run the artisan command php artisan db:seed.

Inside of this class, we have a factory method that takes two arguments. The first one being the path to the class and the second being the number of records we want to save in the database. The create method which is also attached to the factory method as its name implies is the final process in creating our factory. It also accepts optional arguments which can also be used for columns in our database.

Users Table Seeder

Open the UserTableSeeder.php file that was created and replace all the contents with the code provided below.

<?php
use Illuminate\Database\Seeder;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
factory(App\User::class, 10)->create();
}
}
view raw UsersTableSeeder.php hosted with ❤ by GitHub

The code copied is very similar to the code we wrote in the ForumsTableSeeder.php. With that all cleared out, we’d talk about the most important file in making our seeds work which is the DatabaseSeeder.php.

Database Seeder File

This file is responsible for loading and running all of the seeds that we created. We can run the command by utilizing an artisan command, but more of that later. Update the codes inside of the DatabaseSeeder.php file with the git gist below.

<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*
* @return void
*/
public function run()
{
$this->call(UsersTableSeeder::class);
$this->call(ForumsTableSeeder::class);
}
}
view raw DatabaseSeeder.php hosted with ❤ by GitHub

Database Factories

Inside of our database folder, we have a sub-folder called factories. Open opening this folder, you’ll see two files. The ForumFactory.php and the UserFactory.php. One of those files was created from the artisan command we ran earlier. php artisan make:factory **FACTORY_NAME**, while the other file UserFactory.php is provided by Laravel by default.

Updating The ForumFactory.php

The factory file loads in our Forum class, a Faker Class, and a factory class as well.

A method named define is called from the loaded factory class. This method accepts two arguements. The Database Model to use and a callback function with the faker class as a dependency to the callback function.

The faker instance is used to call other properties inside the callback function which returns an array at the end.

With that all cleared out, The next thing as always is to replace/update the code inside of the ForumFactory.php file

<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\Forum;
use Faker\Generator as Faker;
$factory->define(Forum::class, function (Faker $faker) {
return [
'title' => $faker->text,
'content' => $faker->sentence,
'likes' => 0,
'comments' => 0
];
});
view raw ForumFactory.php hosted with ❤ by GitHub

Now that we have completely set up our database, it’s time for us to run some artisan commands and move on to the next phase in this lesson. Run the artisan command listed below by opening up the project directory in your command prompt or terminal.

php artisan migrate
php artisan migrate db:seed
view raw SampleCommands.sh hosted with ❤ by GitHub

If you have problems executing this file, you can replace the first command with php artisan migrate:refresh.

3. Installing The Auth Scaffolding

Open your phpMyAdmin Console in your browser and navigate to the database name created for this lesson. You will find some tables and data all taken care of by Laravel.

The next thing is to create an Auth Scaffolding for our application. Open your project root directory in your command prompt again and enter the following commands.

Depending on your laravel version, things might break but I recommend that you stick to laravel versions from 7 upward.

  1. Run the composer require laravel/ui command with your project directory opened up in your terminal.
  2. Run the npm install command with your project directory opened up in your terminal.
  3. Open your terminal and navigate to this lesson directory and enter the command php artisan ui vue –auth.
  4. Install the following npm packages as well npm install ckeditor4-vue sweetalert vue-router vuex --save

With all those commands successful, I can say we have successfully created an Authentication Scaffold for our application. The next thing we have to do is to create the API and all the necessary Endpoints needed for our client.

4. Creating Controllers

Next up, we will be creating controllers for our application. You can think of the controllers as the major part of our application. Under this lesson, we will be creating several controllers to utilize the models we created earlier.

Creating The Auth Controllers

When you open the Controllers Folder, you’ll find a new folder called Auth. This folder contains all the files needed to make authentication possible for us. But in our case, we will only be updating the LoginController and the RegisterCotroller.

Without much delay, I’d like us to replace the content of the LoginController with the code below.

<?php
namespace App\Http\Controllers\Auth;
use Auth;
use App\User;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
/**
* Authenticates Users...
*
* @return JSON
*/
public function login(Request $request)
{
// Do Some Validation....
$validator = Validator::make($request->all(), [
'email' => ['email', 'required'],
'password' => ['string', 'required']
]);
$Response = array();
// check if the validator fails....
if ($validator->fails()) {
$Response['data'] = [];
$Response['token'] = [];
$Response['status'] = 400;
$Response['errors'] = $validator->errors();
$Response['message'] = 'Sorry, Some Errors Occured And Your Request Could Not Be Completed.';
// send back a json response...
return response()->json($Response, 400);
}
// Since the check was passed, we can check the users email address and password and validate the user...
$emailAddress = stripcslashes(strip_tags($request->input('email')));
$password = stripcslashes(strip_tags($request->input('password')));
if (Auth::attempt(['email' =>$emailAddress, 'password' => $password])) {
$user = User::where('email', $emailAddress)->first();
$Response['data'] = $user;
$Response['token'] = Auth::user()->createToken('authentication')->accessToken;
$Response['status'] = 200;
$Response['errors'] = '';
$Response['message'] = 'Login Successfull.';
// dont forget to update the user updated_at field...
$user->updated_at = date('Y-m-d H:i:s');
$user->save();
// send back a valid json response....
return response()->json($Response, 200);
}
// If there was an email or password mismatch...
$Response['data'] = [];
$Response['token'] = [];
$Response['status'] = 401;
$Response['errors'] = array(
'Email Password' => [
'Failed To Login. Please, Check The Email Address Or Password And Try Again.'
]
); //This was an array of errors in the first conditional... don't reinvent the wheel....
// send back a response...
return response()->json($Response, 401);
}
/**
* Logout user (Revoke the token)
*
* @return [string] message
*/
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out'
]);
}
}
view raw LoginController.php hosted with ❤ by GitHub

The LoginController.php file has one major method that we are concerned about which is the login method. This method loads in the Request Class as a dependency and this Class holds all the incoming requests coming from the client which we will be manipulating. You can check out the official Laravel Documentation to learn more about authentications with Laravel.

The SignupController.php file is responsible for creating new user accounts. Inside of this file or Class, we have one method which is called create. This method also loads in the Request Class as a dependency. This file does some simple validation and it returns JSON content back to the Client. With that out of the way, we can replace the content of the SignupController.php file with the gist provided below.

<?php
namespace App\Http\Controllers\Auth;
use App\User;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
// use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create(Request $request)
{
// Let's leave this here so it can be accessible in all of the blocks...
$Response = array();
try {
// Validate the user account......
$validation = Validator::make($request->all(),[
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:7'],
]);
if ($validation->fails()) {
$Response['data'] = [];
$Response['token'] = [];
$Response['status'] = 400;
$Response['errors'] = $validation->errors();
$Response['message'] = 'Sorry, Some Errors Occured And Your Request Could Not Be Completed.';
// send back a json response...
return response()->json($Response, 400);
}
// create the user....
$user = User::create([
'name' => $request->input('name'),
'email' => $request->input('email'),
'email_verified_at' => date('Y-m-d H:i:s'),
'password' => Hash::make($request->input('password')),
]);
// Prepare and send back a response...
$Response['data'] = $user;
$Response['token'] = $user->createToken('authentication')->accessToken;
$Response['status'] = 201;
$Response['errors'] = '';
$Response['message'] = 'Your Account Has Been Created Successfully.';
// send back a valid json response....
return response()->json($Response, 201);
} catch (Exception $e) {
// Prepare and send back a response.......
$Response['data'] = [];
$Response['status'] = 500;
$Response['errors'] = array(
'message' => $e->getMessage()
);
$Response['token'] = '';
$Response['message'] = 'Failed To Create Your Account. Please, Try Again Later.';
return response()->json($Response, 500);
}
}
}
view raw SignupController.php hosted with ❤ by GitHub

With those two sections out of the way, we can say we have successfully created authentication for our application. Now, the next thing we have to do is creating the other Controllers for this lesson.

Creating The Forums Controller

Under this section, we will create a Controller for the Forums Endpoint. This Endpoint will help us achieve the following features.

  1. Create A New Forum.
  2. Fetch A Specific Forum.
  3. Fetch All Forums.
  4. Update A Forum.
  5. Like A Forum.
  6. Delete A Forum.

For us to start, there is another artisan command I will like to show us. This artisan command will help us create a new controller instead of doing it manually.

Open up the project in your cmd or terminal and enter the command php artisan make:controller ForumsController.

This command will create a new file inside of the controllers folder. Inside of the newly created file, I’d like us to replace the contents of the file with the code provided below.

<?php
namespace App\Http\Controllers;
use App\Forum as ForumModel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ForumsController extends Controller
{
public function fetchForums(Request $Request)
{
$Response = array();
$forums = ForumModel::orderBy('id', 'desc')->get();
$forums = collect($forums)->map(function ($el) {
$el->user = $el->user;
$el->humanTime = $el->created_at->diffForHumans();
return $el;
});
// check if the forums are not empty,,,
if (count($forums) > 0) {
$Response['status'] = 200;
$Response['message'] = 'Successfully Fetched ' . count($forums) . ' records.';
$Response['data'] = $forums;
$Response['errors'] = [];
return response()->json($Response, 200);
}
// Return a 204 Response header...
return response()->json($Response, 204);
}
public function create(Request $Request)
{
$Response = array();
$validator = Validator::make($Request->all(), [
'title' => ['string', 'required'],
'content' => ['string', 'required']
]);
// check if validator fails...
if ($validator->fails()) {
$Response['data'] = [];
$Response['status'] = 400;
$Response['errors'] = $validator->errors();
$Response['message'] = 'Some Errors Occured. Please, Try Again Later.';
return response()->json($Response, 400);
}
// proceed with the next request...
$user_id = $Request->user()->id;
$title = stripcslashes(strip_tags($Request->input('title')));
$content = $Request->input('content');
$likes = 0;
$comments = 0;
// Let's cook some forums...
try {
$Forum = ForumModel::create([
'title' => $title,
'likes' => $likes,
'content' => $content,
'user_id' => $user_id,
'comments' => $comments
]);
// send back some data...
$Response['data'] = $Forum;
$Response['data']['humanTime'] = $Forum->created_at->diffForHumans();
$Response['status'] = 201;
$Response['errors'] = '';
$Response['message'] = 'Your Forum Has Been Created Successfully.';
return response()->json($Response, 201);
} catch (Exception $e) {
// send back the error message.........
$Response['data'] = array();
$Response['status'] = 500;
$Response['errors'] = array(
'title' => 'Sorry, An Unexpected Error Occurred And Your Request Could Not Be Completed.',
'message' => $e->getMessage()
);
$Response['message'] = 'Sorry, Some Errors Occurred And Your Forum Couldn\'t Be Created.';
return response()->json($Response, 500);
}
}
public function read($forumId)
{
$Response = array();
// fetch the record asap!!!
try {
$forum = ForumModel::find((Integer) ($forumId));
// throw in some conditionals...
if (empty($forum)) {
return response()->json(array(), 204);
}
// send back some data...
$Response['data'] = $forum;
$Response['data']['user'] = $forum->user;
$Response['status'] = 200;
$Response['errors'] = '';
$Response['timestamp'] = $forum->created_at->diffForHumans();
$Response['message'] = 'Successfully Retrieved ' . $forum->title;
return response()->json($Response, 200);
} catch (Exception $e) {
// send back the error message.........
$Response['data'] = array();
$Response['status'] = 500;
$Response['errors'] = array(
'title' => 'Sorry, An Unexpected Error Occurred And Your Request Could Not Be Completed.',
'message' => $e->getMessage()
);
$Response['message'] = '';
return response()->json($Response, 500);
}
}
public function update(Request $Request, $forumId)
{
$Response = array();
$validator = Validator::make($Request->all(), [
'title' => ['string', 'required'],
'content' => ['string', 'required']
]);
// check for errors and send back a 400 HTTP Header...
if ($validator->fails()) {
$Response['data'] = [];
$Response['status'] = 400;
$Response['errors'] = $validator->errors();
$Response['message'] = 'Some Errors Occured. Please, Try Again Later.';
return response()->json($Response, 400);
}
try {
// Check if the forum is valid...
$forumId = stripcslashes(strip_tags((Integer) $forumId));
$Forum = ForumModel::where('id', $forumId)->where('user_id', $Request->user()->id)->first();
if (empty($Forum)) {
return response()->json(array(), 204);
}
// Update The Forum.......
$Forum->title = stripcslashes(strip_tags($Request->title));
$Forum->content = $Request->content;
$Forum->save();
// send back some data...
$Response['data'] = $Forum;
$Response['status'] = 200;
$Response['errors'] = '';
$Response['message'] = 'Your Thread Has Been Updated Successfully';
return response()->json($Response, 200);
} catch (Exception $e) {
// send back the error message.........
$Response['data'] = array();
$Response['status'] = 500;
$Response['errors'] = array(
'title' => 'Sorry, An Unexpected Error Occurred And Your Request Could Not Be Completed.',
'message' => $e->getMessage()
);
$Response['message'] = '';
return response()->json($Response, 500);
}
}
public function delete(Request $request, $forumId)
{
$Response = array();
try {
$forumId = stripcslashes(strip_tags($forumId));
$forum = ForumModel::find($forumId);
$comments = $forum->commentModel;
// delete all the comments first... #Delete the forum next...
if (count($comments) > 0) {
foreach ($comments as $comment) {
$comment->delete();
}
}
$forum->delete();
// send back some data...
$Response['data'] = array();
$Response['status'] = 200;
$Response['errors'] = '';
$Response['message'] = 'The Selected Forum And It\'s Associated Comments Has Been Deleted Successfully.';
return response()->json($Response, 200);
} catch (Exception $e) {
// send back the error message.........
$Response['data'] = array();
$Response['status'] = 500;
$Response['errors'] = array(
'title' => 'Sorry, An Unexpected Error Occurred And Your Request Could Not Be Completed.',
'message' => $e->getMessage()
);
$Response['message'] = '';
return response()->json($Response, 500);
}
}
public function like($forumId)
{
$Response = array();
try {
$forum = ForumModel::find($forumId);
// checkif the forum exits...
if (empty($forum)) {
return response()->json([], 204);
}
$forum->likes = (Integer) $forum->likes + 1;
$forum->save();
$Response['data'] = $forum;
$Response['errors'] = [];
$Response['status'] = 200;
$Response['message'] = 'Your Action Has Been Saved Successfully.';
return response()->json($Response, 200);
} catch (Exception $e) {
// send back the error message.........
$Response['data'] = array();
$Response['status'] = 500;
$Response['errors'] = array(
'title' => 'Sorry, An Unexpected Error Occurred And Your Request Could Not Be Completed.',
'message' => $e->getMessage()
);
$Response['message'] = '';
return response()->json($Response, 500);
}
}
}
view raw ForumController.php hosted with ❤ by GitHub

Creating The Comments Controller

Now that we have created a controller for our forum, I think it’s only fair to create another controller for our comments since there will be some comments.

With that said, let’s open the project in a new terminal window and execute the command below. php artisan make:controller CommentsController. This will also create a new controller inside of the controller folder. Replace the content of the created CommentsController with the code snippet provided below.

<?php
namespace App\Http\Controllers;
use App\Forum as ForumModel;
use App\Comment as CommentModel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class CommentsController extends Controller
{
public function create(Request $Request, $forumId)
{
$Response = array();
$user_id = $Request->user()->id;
$forumId = stripcslashes(strip_tags($forumId));
$comment = $Request->input('comment');
try {
$comment = CommentModel::create([
'user_id' => $user_id,
'forum_id' => $forumId,
'comment' => $comment
]);
// Update the forum Comments
$forumComments = ForumModel::find($forumId);
$forumComments->comments = (Integer) $forumComments->comments + 1;
$forumComments->save();
// Send Back A JSON Response......
$Response['data'] = $comment;
$Response['data']['user'] = $comment->user;
$Response['humanTime'] = $comment->created_at->diffForHumans();
$Response['status'] = 201;
$Response['errors'] = '';
$Response['message'] = 'Comment Has Been Saved Successfully.';
return response()->json($Response, 201);
} catch (Exception $e) {
// send back the error message.........
$Response['data'] = $Forum;
$Response['status'] = 500;
$Response['errors'] = array(
'title' => 'Sorry, An Unexpected Error Occurred And Your Request Could Not Be Completed.',
'message' => $e->getMessage()
);
$Response['message'] = '';
return response()->json($Response, 500);
}
}
public function fetchComments(Request $Request, $forumId)
{
$Response = array();
// Fetch all comments under this forum...
$comments = CommentModel::where('forum_id', $forumId)->orderBy('id', 'desc')->get();
if (count($comments) > 0) {
$comments = collect($comments)->map(function ($el) {
$el->user = $el->user;
$el->humanTime = $el->created_at->diffForHumans();
return $el;
});
$Response['status'] = 200;
$Response['comments'] = $comments;
$Response['message'] = 'Successfully fetched ' . count($comments) . ' comments';
$Response['errors'] = [];
return response()->json($Response, 200);
}
return response()->json($Response, 204);
}
}

We have been able to create controllers for our application, and the next thing we will be looking at is creating routes or Endpoints for our application.

5. Creating Routes

There is an api.php file inside of our routes folder in the project root directory. Open up that file and replace the code with the snippet provided below.

<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
// Authentication Routes...
Route::post('/v1/login', 'Auth\LoginController@login');
Route::get('/v1/logout', 'Auth\LoginController@logout');
// Registration Routes...
Route::post('/v1/register', 'Auth\RegisterController@create');
// Forum Routes....
Route::middleware('auth:api')->post('/v1/forum/create', 'ForumsController@create');
Route::middleware('auth:api')->post('/v1/forum/update/{forumId}', 'ForumsController@update');
Route::middleware('auth:api')->get('/v1/forum', 'ForumsController@fetchForums');
Route::middleware('auth:api')->get('/v1/forum/like/{forumId}', 'ForumsController@like');
Route::middleware('auth:api')->get('/v1/forum/comments/{forumId}', 'ForumsController@read');
Route::middleware('auth:api')->delete('/v1/forum/delete/{forumId}', 'ForumsController@delete');
// Comment Routes....
Route::middleware('auth:api')->post('/v1/comment/{forumId}', 'CommentsController@create');
Route::middleware('auth:api')->get('/v1/comments/{forumId}', 'CommentsController@fetchComments');
// Token Verification || Confirmation...
Route::middleware('auth:api')->get('/v1/user', function (Request $request) {
return $request->user();
});
view raw api.php hosted with ❤ by GitHub

6. Writing Vuejs

This is the final section in building our application. We will be using Vuejs for rendering our pages instead of Laravel Blade. Inside of the js folder in the resources folder. We will create some folders namely:

  1. components
  2. includes
  3. routes
  4. views

I decided to use skeleton css as my Css Framework for this lesson and the style assets are in the Css folder inside the public directory. You can use any Css Framework of choice provided you know your way around the framework.

We will also create a new file in the js folder root directory. The file will be our Base Template and it will be called App.vue. After creating the file, I’d like us to write the code below into the App.vue file.

<template>
<main>
<router-view></router-view>
</main>
</template>
<script type="text/javascript">
export default {}
</script>
view raw App.vue hosted with ❤ by GitHub

The router-view tag is where all of our views will be created depending on the web page the user is visiting. The next step is to create our Base Nav because we want to have the same header on all of our pages.

7. The Base Nav

Inside of the newly created includes folder, I’d like us to create a new Vue file and call it Nav.vue. This is because we want to have consistent Navigation across all of our web pages. After that open the created Nav.vue inside of the includes folder and replace add the following codes to it.

<template>
<nav>
<div class="container">
<div class="row">
<div class="six columns">
<a href="/" class="application-title"><i class="material-icons">sms</i> Ld-Talk</a>
</div>
<div class="six columns">
<ul v-if="!isLoggedIn">
<li><router-link :to="{ name: 'Welcome' }"><i class="material-icons">lock_open</i> Login</router-link></li>
<li><a href=""><i class="material-icons">local_cafe</i> Buy Me Coffee</a></li>
</ul>
<ul v-else>
<li><router-link :to="{ name: 'Welcome' }"><i class="material-icons">home</i> Home</router-link></li>
<li><a href=""><i class="material-icons">lock_open</i> Logout</a></li>
<li><a href=""><i class="material-icons">local_cafe</i> Buy Me Coffee</a></li>
</ul>
</div>
</div>
</div>
</nav>
</template>
<script type="text/javascript">
import axios from 'axios';
export default {
data() {
return {
isLoggedIn: false
}
},
created() {
let userSecret = localStorage.getItem('access-token');
if (userSecret.trim() !== '') {
this.isLoggedIn = true;
return;
}
this.isLoggedIn = false;
}
}
</script>
view raw Nav.vue hosted with ❤ by GitHub

A basic Vuejs file is divided into 3 sections namely;

  1. Template: This is what is inserted into the DOM and it contains your HTML Codes and sometimes you’d see other Vuejs Component used here as a component job is to render a basic HTML page.

  2. Script: This is where all our business logic goes. We have access to lifecycle hooks which can help us render actions at a point in time when the Component is being created.

  3. Style: This is where our CSS styles go and another great thing here is that you can use Css preprocessors as well.

8. Creating The Welcome Component

Inside of the components folder we created earlier, I’d like us to create a new file and call it WelcomeComponent.vue. This file is going to act as our web app home page, login page, and also the_signup page_.

After creating the file, copy, and paste the codes below into the file.

<template>
<div>
<header>
<Nav></Nav>
</header>
<section class="app_description">
<div class="container">
<div class="row">
<div class="six columns login-column">
<h5>Login to your <b>ld-talk</b> account.</h5>
<hr>
<div class="card-body">
<form method="POST" @submit.prevent="accountLogin()">
<div class="form-group">
<label for="login-email" class="col-md-4 col-form-label text-md-right">Email Address</label>
<div class="col-md-6">
<input id="login-email" type="email" class="form-control" name="email" v-model="login.email" required autocomplete="email" autofocus>
</div>
</div>
<div class="form-group">
<label for="login-password" class="col-md-4 col-form-label text-md-right">Password</label>
<div class="col-md-6">
<input id="login-password" v-model="login.password" type="password" class="form-control" name="password" required autocomplete="current-password">
</div>
</div>
<div class="form-group mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
Login
</button>
</div>
</div>
</form>
</div>
</div>
<div class="six columns signup-column">
<h5>Create an <b>ld-talk</b> account. It's <b>free.</b></h5>
<hr>
<form method="POST" @submit.prevent="createAccount()">
<div class="form-group">
<label for="email" class="col-md-4 col-form-label text-md-right">Name</label>
<div class="col-md-6">
<input type="text" class="form-control" name="name" required autocomplete="name" autofocus v-model="register.name">
</div>
</div>
<div class="form-group">
<label for="email" class="col-md-4 col-form-label text-md-right">Email Address</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control" name="email" required autocomplete="email" autofocus v-model="register.email">
</div>
</div>
<div class="form-group">
<label for="password" class="col-md-4 col-form-label text-md-right">Password</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control" name="password" required autocomplete="current-password" v-model="register.password">
</div>
</div>
<div class="form-group mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
Signup
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</section>
</div>
</template>
<script type="text/javascript">
import Nav from '../includes/Nav';
import axios from 'axios';
import swal from 'sweetalert';
export default {
components: {
Nav
},
methods: {
async createAccount () {
if (this.register.name.trim() == '') {
swal('Name Error', 'Sorry, This Field Cannot Be Empty.', 'error');
return;
}
if (this.register.email.trim() == '') {
swal('Email Error', 'Sorry, This Field Cannot Be Empty', 'error');
return;
}
if (this.register.password.length < 7) {
swal('Password Error', 'Please, Use A Stronger Password', 'error');
return;
}
// Make an xhr request to create the account....
try {
let user = await axios.post(this.$baseUrl + 'register', this.register, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
}
});
// check the http status code....
if (user.status == 201) {
swal('Account Created', 'Congratulations. Your ld-talk account has been created Successfully.', 'success');
// clear the form fields..........
this.register.name = '';
this.register.email = '';
this.register.password = '';
// set some storage and all.....
localStorage.setItem('access-token', user.data.token);
localStorage.setItem('user-id', user.data.data.id);
setTimeout(() => {
this.$router.push({
name: 'Dashboard'
});
}, 1000);
return;
}
} catch (e) {
let errorData = e.response.data;
if (errorData.status == 400) {
for (const error in errorData.errors) {
swal(
`${error} Error: `,
errorData.errors[error][0],
'error'
);
}
return;
}
swal('Registration Failed', errorData.message, 'error');
return;
}
},
async accountLogin() {
// Validate credentials....
if (this.login.email.trim() == '') {
swal('Email Error', 'Sorry, The Email Field Cannot Be Empty.', 'errors');
return;
}
if (this.login.password.length < 7) {
swal('Invalid Credentials', 'Please, Check The Email Or Password And Try Again.', 'errors');
return;
}
// make an xhr request...........
try {
let user = await axios.post(this.$baseUrl + 'login', this.login, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}
});
// check the http status code...
if (user.status == 200) {
swal('Login Successfull', `Welcome Back ${user.data.data.name}.`, 'success');
// clear the form fields..........
this.login.email = '';
this.login.password = '';
// set some storage and all.....
localStorage.setItem('access-token', user.data.token);
localStorage.setItem('user-id', user.data.data.id);
setTimeout(() => {
this.$router.push({
name: 'Dashboard'
});
}, 1000);
return;
}
} catch (e) {
let errorData = e.response.data;
if (errorData.status == 400 || errorData.status == 401) {
for (const error in errorData.errors) {
swal(
`${error} Error: `,
errorData.errors[error][0],
'error'
);
}
return;
}
swal('Login Failed', errorData.message, 'error');
return;
}
}
},
async created() {
try {
let userSecret = localStorage.getItem('access-token');
// check if the user access token is set.....
if (userSecret.trim() !== '') {
let user = await axios.get(this.$baseUrl + 'user', {
headers: {
Accept: 'application/json',
'Authorization': 'Bearer ' + userSecret,
'Cache-Control': 'no-cache'
}
});
// check and redirect the user...
if (user.status == 200) {
setTimeout(() => {
this.$router.push({
name: 'Dashboard'
});
}, 1000);
}
}
} catch (e) {
}
},
data() {
return {
register: {
name: '',
email: '',
password: '',
},
login: {
email: '',
password: ''
}
}
}
}
</script>
<style scoped>
.form-control {
width: 100% !important;
}
.login-column {
padding-right: 100px;
border-right: 1px solid grey;
}
</style>
view raw WelcomeComponent.vue hosted with ❤ by GitHub

Inside of this file, we have our template section which contains HTML Elements that help us create our Login Form and our Signup Form.

In our scripts section, we require our Nav.vue file which contains our Navbar Codes. There are also some ** third-party packages such as sweetalert and axios** which are imported into the file. We have several objects such as;

  1. Components: This is where other components that we will like to reuse stay.

  2. Methods: This is where all our methods will stay. Inside of this object, we have several methods and some of them are responsible for authenticating a user. The methods provided here make an HTTP Request to the Endpoints we created earlier in our Controllers.

  3. Lifecycle Hook: We have a created lifecycle hook and this hook is fired when our component has been created. Inside of this hook, we are checking our localstorage for a value to see if the user is logged in or not.

  4. Data Object: We have our data object inside of this data property and this is used for storing data that will be utilized in the template above or in some of our methods.

  5. Style: We have some CSS Style which is used only in this Component for tweaking some things.

9. The Dashboard Component

This is where the authenticated user goes and this Component is also responsible for managing CRUD features or functionalities for the Forums we created earlier. Create a new file in the components folder and name it DashboardComponent.vue. Copy the github gist below into the file.

<template id="">
<div>
<header>
<Nav></Nav>
</header>
<section class="app_description">
<div class="container">
<div class="row">
<div class="six columns forums-column">
<template v-if="type !== 'comments'">
<h5><i class="material-icons">sms</i> Topics</h5>
<p>The Most Recent Topics For You To Talk About...</p>
<hr>
</template>
<template v-if="type == 'comments'">
<!-- Add The Post Meta Here!! -->
<div class="card">
<div class="card-body">
<h5 class="card-title"><b>{{ forum.title }}</b></h5>
<h6 class="card-subtitle mb-2 text-muted"><b>Author: {{ forum.user.name }}, Created {{ forum.timestamp }}</b></h6>
<hr>
<p class="card-text" v-html="forum.content"></p>
<div class="card-footer">
<ul>
<li class="activity forums" @click.prevent="like(forum.id)"><a href="#" class="card-link"><i class="material-icons">favorite</i> {{ forum.likes }}</a></li>
<li class="activity forums" @click.prevent="editForum(forum)"><a href="#" class="card-link"><i class="material-icons">edit</i> Edit</a></li>
</ul>
</div>
</div>
</div>
<h3><i class="material-icons medium">sms</i> Comments {{ forum.comments }}</h3>
<hr>
<!-- Add The Comments Here... -->
<div v-if="comments.length > 0">
<div class="card" v-for="postComment in comments" :key="postComment.id">
<div class="card-body">
<p><b>{{ postComment.user.name }} Created {{ postComment.humanTime }}</b></p>
<p v-html="postComment.comment"></p>
</div>
</div>
</div>
</template>
<template v-if="forums.length > 0 && type !== 'comments'">
<div class="forums" v-for="singleForum in forums" :key="singleForum.id">
<div class="card">
<div class="card-body">
<h5 class="card-title"><b>{{ singleForum.title }}</b></h5>
<h6 class="card-subtitle mb-2 text-muted"><b>Author: {{ singleForum.user.name }}, Created {{ singleForum.humanTime }}</b></h6>
<hr>
<p class="card-text" v-html="$options.filters.truncate(singleForum.content)"></p>
<div class="card-footer">
<ul>
<li class="activity forums" @click.prevent="like(singleForum.id)"><a href="#" class="card-link"><i class="material-icons">favorite</i> {{ singleForum.likes }}</a></li>
<li v-if="parseInt(singleForum.user.id) == user_id" @click.prevent="fetchAndAddComments(singleForum)" class="activity forums"><a href="#" class="card-link"><i class="material-icons">sms</i> {{ singleForum.comments }}</a></li>
<li class="activity forums" @click.prevent="editForum(singleForum)"><a href="#" class="card-link"><i class="material-icons">edit</i> Edit</a></li>
<li v-if="singleForum.user.id == user_id" class="activity forums" @click.prevent="deleteForum(singleForum.id)"><a href="#" class="card-link"><i class="material-icons">close</i></a></li>
</ul>
</div>
</div>
</div>
</div>
</template>
</div>
<div class="six columns">
<div class="forum-editor">
<h5 v-if="type === 'create'"><i class="material-icons">sms</i> Create A Topic...</h5>
<h5 v-if="type === 'edit'"><i class="material-icons">sms</i> <b>Update {{ forum.title }}</b>...</h5>
<h5 v-if="type === 'comments'"><i class="material-icons">sms</i> <b> Add A Comment.</b></h5>
<hr>
<form v-if="type === 'create'" method="post" @submit.prevent="createForum()">
<div class="form-group">
<label for="forum-title" class="col-md-4 col-form-label text-md-right">Forum Title</label>
<div class="col-md-6">
<input id="forum-title" type="text" class="form-control" name="text" required autofocus v-model="forum.title">
</div>
</div>
<div class="form-group">
<label for="forum-content" class="col-md-4 col-form-label text-md-right">Forum Content</label>
<div class="col-md-6">
<ckeditor v-model="forum.content" required placeholder="Start A Topic..." :config="editorConfig"></ckeditor>
</div>
</div>
<div class="form-group btn-div mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
Create Topic
</button>
</div>
</div>
</form>
<form v-if="type === 'edit'" method="post" @submit.prevent="updateForum()">
<div class="form-group">
<label for="forum-title" class="col-md-4 col-form-label text-md-right">Forum Title</label>
<div class="col-md-6">
<input id="forum-title" type="text" class="form-control" name="text" required autofocus v-model="forum.title">
</div>
</div>
<div class="form-group">
<label for="forum-content" class="col-md-4 col-form-label text-md-right">Forum Content</label>
<div class="col-md-6">
<ckeditor v-model="forum.content" required placeholder="Start A Topic..." :config="editorConfig"></ckeditor>
</div>
</div>
<div class="form-group btn-div mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
Update Topic
</button>
</div>
</div>
</form>
<form v-if="type === 'comments'" @submit.prevent="addComment()" method="post" >
<div class="form-group">
<label for="forum-content" class="col-md-4 col-form-label text-md-right">Your Comment</label>
<div class="col-md-6">
<ckeditor v-model="comment" required placeholder="Enter A Comment..." :config="editorConfig"></ckeditor>
</div>
</div>
<div class="form-group btn-div mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
Comment
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script type="text/javascript">
import axios from 'axios';
import swal from 'sweetalert';
import Nav from '../includes/Nav';
export default {
data() {
return {
editorConfig: {},
forum: {
id: null,
user: {},
title: '',
content: '',
likes: 0,
comments: 0,
timestamp: ''
},
type: 'create',
forums: [],
comment: '',
user_id: '',
comments: [],
accessToken: '',
}
},
components: {
Nav
},
async mounted() {
await this.fetchForums();
this.user_id = parseInt(localStorage.getItem('user-id'));
},
filters: {
truncate(value) {
// Make sure an element and number of items to truncate is provided
if (!value) return;
// Get the inner content of the element
var content = value.trim();
// Convert the content into an array of words
// Remove any words above the limit
if (value.length > 25) {
content = content.split(' ').slice(0, 25);
// Convert the array of words back into a string
content = content.join(' ') + '.....';
return content;
}
return value;
},
},
methods: {
async createForum() {
try {
if (this.forum.content.trim() == '') {
swal('Forum Content', 'Sorry, This Field Cannot Be Empty. Please, Write A Story To Continue.', 'error');
return;
}
// create a simple forum..........
let createdForum = await axios.post(this.$baseUrl + 'forum/create', this.forum, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (createdForum.status == 201) {
swal('Topic Created.', 'Your Topic Has Been Created Successfully', 'success');
this.forums.unshift(createdForum.data.data);
return;
}
} catch (e) {
let errorData = e.response.data;
if (errorData.status == 400) {
for (const error in errorData.errors) {
swal(
`${error} Error: `,
errorData.errors[error][0],
'error'
);
}
return;
}
swal('Operation Failed', errorData.message, 'error');
return;
}
},
async updateForum() {
try {
if (this.forum.content.trim() == '') {
swal('Forum Content', 'Sorry, This Field Cannot Be Empty. Please, Write A Story To Continue.', 'error');
return;
}
// create a simple forum..........
let updatedForum = await axios.post(this.$baseUrl + 'forum/update/' + this.forum.id, this.forum, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (updatedForum.status == 200) {
swal('Topic Updated.', 'Your Topic Has Been Updated Successfully', 'success');
this.forums = this.forums.map((el) => {
if (parseInt(el.id) == parseInt(this.forum.id)) {
el.title = this.forum.title;
el.content = this.forum.content;
}
return el;
});
this.type = 'create';
this.forum.id = null;
this.forum.title = '';
this.forum.content = '';
return;
}
} catch (e) {
let errorData = e.response.data;
if (errorData.status == 400) {
for (const error in errorData.errors) {
swal(
`${error} Error: `,
errorData.errors[error][0],
'error'
);
}
return;
}
swal('Operation Failed', errorData.message, 'error');
return;
}
},
async fetchForums() {
try {
let forums = await axios.get(this.$baseUrl + 'forum', {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (forums.status == 200) {
this.forums = forums.data.data;
return;
}
} catch (e) {
this.forums = [];
return;
}
},
async fetchAndAddComments(forumComment) {
try {
let forumAndComment = await axios.get(this.$baseUrl + 'forum/comments/' + forumComment.id, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (forumAndComment.status == 200) {
this.type = 'comments';
this.forum.id = forumAndComment.data.data.id;
this.forum.title = forumAndComment.data.data.title;
this.forum.content = forumAndComment.data.data.content;
this.forum.likes = forumAndComment.data.data.likes;
this.forum.comments = forumAndComment.data.data.comments;
this.forum.timestamp = forumAndComment.data.timestamp;
this.forum.user = forumAndComment.data.data.user;
await this.fetchComments(forumComment.id);
return;
}
} catch (e) {
this.type = 'create';
this.forum.id = '';
this.forum.title = '';
this.forum.content = '';
this.forum.likes = '';
this.forum.comments = '';
this.forum.timestamp = '';
return;
}
},
async fetchComments(forumId) {
try {
let comments = await axios.get(this.$baseUrl + 'comments/' + forumId, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (comments.status == 200) {
this.comments = comments.data.comments;
return;
}
this.comments = [];
return;
} catch (e) {
console.log(e);
this.comments = [];
return;
}
},
async editForum(forumToUpdate) {
this.type = 'edit';
this.forum.id = forumToUpdate.id;
this.forum.title = forumToUpdate.title;
this.forum.content = forumToUpdate.content;
},
async addComment() {
try {
let comment = await axios.post(this.$baseUrl + 'comment/' + this.forum.id, {
comment: this.comment
}, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (comment.status == 201) {
swal('Success', 'Your Comment Has Been Saved Successfully.', 'success');
this.comments.unshift(comment.data.data);
this.forum.comments += 1;
return;
}
swal('Operation Failed', 'Oops, An Unexpected Error Occurred And Your Comment Could Not Be Saved.', 'error');
return;
} catch (e) {
let errorData = e.response.data;
if (errorData.status == 400) {
for (const error in errorData.errors) {
swal(
`${error} Error: `,
errorData.errors[error][0],
'error'
);
}
return;
}
swal('Operation Failed', errorData.message, 'error');
return;
}
},
async like(forumId) {
try {
let liked = await axios.get(this.$baseUrl + 'forum/like/' + forumId, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (liked.status == 200) {
this.forums = this.forums.map((el) => {
if (parseInt(el.id) == parseInt(forumId)) {
el.likes += 1;
}
return el;
});
swal({
icon: 'success'
});
return;
}
} catch (e) {
this.forums = [];
return;
}
},
async deleteForum(forumId) {
try {
let deletedForum = await axios.delete(this.$baseUrl + 'forum/delete/' + forumId, {
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.accessToken,
'Cache-Control': 'no-cache'
}
});
if (deletedForum.status == 200) {
swal('success', 'The Selected Forum Has Been Deleted Successfully!', 'success');
this.forums = this.forums.filter((el) => {
if (el.id == forumId) return false;
})
return;
}
swal('Operation Failed', 'The Selected Forum Could Not Be Deleted Successfully. Please, Try Again Later!', 'error');
return;
} catch (e) {
let errorData = e.response.data;
if (errorData.status == 400) {
for (const error in errorData.errors) {
swal(
`${error} Error: `,
errorData.errors[error][0],
'error'
);
}
return;
}
swal('Operation Failed', errorData.message, 'error');
return;
}
}
},
async created() {
try {
let userSecret = localStorage.getItem('access-token');
this.accessToken = userSecret;
// check if the user access token is set.....
if (userSecret.trim() !== '') {
let user = await axios.get(this.$baseUrl + 'user', {
headers: {
Accept: 'application/json',
'Authorization': 'Bearer ' + userSecret,
'Cache-Control': 'no-cache'
}
});
// check for a status code other than 200.....
if (user.status != 200) {
swal('Unauthorized Access', 'Sorry, You Do Not Have Enough Priviledge To Access This Page.', 'error');
setTimeout(() => {
this.$router.push({
name: 'Welcome'
});
}, 1000);
return;
}
}
if (userSecret.trim() == '') {
swal('Unauthorized Access', 'Sorry, You Do Not Have Enough Priviledge To Access This Page.', 'error');
setTimeout(() => {
this.$router.push({
name: 'Welcome'
});
}, 1000);
return;
}
} catch (e) {
swal('Unauthorized Access', 'Sorry, You Do Not Have Enough Priviledge To Access This Page.', 'error');
setTimeout(() => {
this.$router.push({
name: 'Welcome'
});
}, 1000);
return;
}
}
}
</script>
<style scoped>
.form-control {
width: 100% !important;
}
.forums-column {
padding-right: 100px;
border-right: 1px solid grey;
}
.forum-editor, .forums {
padding: 10px 10px;
}
.app_description {
margin-left: -1%;
}
.btn-div {
margin-top: 15px;
}
.forums {
border-radius: 4px;
margin: 20px 0;
}
.card {
cursor: pointer;
}
ul {
list-style: none;
display: inline-flex;
}
ul li {
margin: 0 5px !important;
}
.activity {
padding: 5px 5px !important;
}
.material-icons {
vertical-align: middle;
}
a {
text-decoration: none;
}
.activity:nth-of-type(1) a,
.activity:nth-of-type(4) a {
color: red;
font-weight: 600;
}
</style>

Inside of this file, we have our template section which is where our HTML Code goes and also other Components as well. Now, our scripts section under this component is where all of the major code lies.

Inside of the methods object, we have several methods that make HTTP Request to the ENDPOINTS we created earlier which creates, updates, fetch, and delete forums. It also has methods that make HTTP Request to fetch comments for each of the forums.

10. Creating The Views

Our Components has been created. Now it’s time for us to create our views. These views are the files that will be loaded into our routers. Let’s get to creating our views right away.

The Welcome View

Create a new file in the views folder and name it Welcome.vue. This file is going to be responsible for loading in our WelcomeComponent that was created earlier. This is because it’s better for us to separate the logic for our application so it will be easier to update. With that clear, paste the code below into the Welcome.vue file.

<template>
<main>
<WelcomeComponent></WelcomeComponent>
</main>
</template>
<script type="text/javascript">
import WelcomeComponent from '../components/WelcomeComponent';
export default {
components: {
WelcomeComponent
}
}
</script>
view raw Welcome.vue hosted with ❤ by GitHub

The Dashboard View

Let’s repeat the same step for the welcome view by creating a new file called Dashboard.vue in the views folder. This file also loads in our Dashboard Component which will be rendered in the Dashboard.vue template section before it is inserted into the DOM.

Copy the code below into the Dashboard.vue file.

<template>
<main>
<DashboardComponent></DashboardComponent>
</main>
</template>
<script type="text/javascript">
import DashboardComponent from '../components/DashboardComponent';
export default {
components: {
DashboardComponent
}
}
</script>
view raw Dashboard.vue hosted with ❤ by GitHub

11. The Routes

Inside of this folder, create a new file and call it index.js. This file is going to load in all of the files in our views folder so that they can be rendered to the DOM depending on the HTTP Path requested. Copy and paste the code below into the file.

import Welcome from '../views/Welcome';
import Dashboard from '../views/Dashboard';
export default [
{ path: '/', alias: '', name: 'Welcome', component: Welcome },
{ path: '/home', alias: '/dashboard', name: 'Dashboard', component: Dashboard },
];
view raw index.js hosted with ❤ by GitHub

12. The App Core

Inside of our Js folder root directory, there is a file called app.js. Replace the content of the file with the code snippet provided below.

/**
* First we will load all of this project's JavaScript dependencies which
* includes Vue and other libraries. It is a great starting point when
* building robust, powerful web applications using Vue and Laravel.
*/
window.Vue = require('vue');
// Imports....
import VueRouter from 'vue-router';
import CKEditor from 'ckeditor4-vue';
import Routes from './routes/index';
// Engage VueJs Plugins....
Vue.use(VueRouter);
Vue.use(CKEditor);
// Router Configurations....
const Router = new VueRouter({
mode: 'hash',
routes: Routes
})
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
Vue.component('App', require('./App.vue').default);
Vue.prototype.$baseUrl = 'http://localhost:8000/api/v1/&#39;;
// Application Core...
const app = new Vue({
el: '#app',
router: Router,
});
view raw app.js hosted with ❤ by GitHub

We are almost done with our application. The next thing we have to do is modify one of Laravel’s Blade File. We will be editing the welcome.blade.php. This file is located in the resources folder inside of a sub-folder called views. Replace the content of that file with the code below.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name', 'Ld-Talk') }}</title>
<meta name="application-name" content="{{ config('app.name', 'Ld-Talk') }}">
<!-- Fonts -->
<link href="//fonts.googleapis.com/css?family=Raleway:400,300,600" rel="stylesheet" type="text/css">
<!-- Icons -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Css -->
<link href="{{ asset('css/custom.css') }}" type="text/css" rel="stylesheet">
<link href="{{ asset('css/normalize.css') }}" type="text/css" rel="stylesheet">
<link href="{{ asset('css/skeleton.css') }}" type="text/css" rel="stylesheet">
</head>
<body>
<div id="app">
<App></App>
</div>
<!-- Javascript -->
<script src="{{ asset('js/app.js') }}" charset="utf-8"></script>
</body>
</html>
view raw welcome.blade.php hosted with ❤ by GitHub

13. Testing Locally

The next thing we have to do is to test our code locally. This is to ensure that the code works before we finally deploy the code to Heroku. Run the command below in separate terminal windows with the project path opened up.

  1. php artisan migrate

  2. php artian passport:install --force

  3. npm run watch

  4. php artisan serve

If everything works fine, Open up your browser and navigate to http://127.0.0.1:8000. The application then will now boot and render our page. You can create a few accounts to test and also create some forums and some comments as well. This is the view of the finished application on my system.

Meanwhile, you can get the complete development code for this tutorial from Github.

Ld Fullstack Image

Ld Fullstack Image II

14. Deploying To Heroku

The final step in this lesson is to deploy our code to a live server. We will be using Heroku and I assume you already have an Heroku account. Please, I advise that you follow this step by step guide.

Step 1

  1. Install The Heroku Cli.

  2. Login to the CLI from your terminal with the command heroku login.

  3. Replace the Package.json file in the project root directory with the gist below.

    {
    "private": true,
    "scripts": {
    "dev": "npm run development",
    "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch": "npm run development -- --watch",
    "watch-poll": "npm run watch -- --watch-poll",
    "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --disable-host-check --config=node_modules/laravel-mix/setup/webpack.config.js",
    "prod": "npm run production",
    "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "start": "npm run prod"
    },
    "engines": {
    "node": "14.3.0",
    "npm": "6.14.8"
    },
    "devDependencies": {
    "axios": "^0.19",
    "bootstrap": "^4.0.0",
    "cross-env": "^7.0",
    "jquery": "^3.2",
    "laravel-mix": "^5.0.1",
    "lodash": "^4.17.19",
    "popper.js": "^1.12",
    "resolve-url-loader": "^2.3.1",
    "sass": "^1.20.1",
    "sass-loader": "^8.0.0",
    "vue": "^2.5.17",
    "vue-template-compiler": "^2.6.10"
    },
    "dependencies": {
    "ckeditor4-vue": "^1.1.0",
    "sweetalert": "^2.1.2",
    "vue-router": "^3.3.4",
    "vuex": "^3.4.0"
    }
    }
    view raw Package.json hosted with ❤ by GitHub
  4. Replace the composer.json file in the project root directory with the github gist provided below.

    {
    "name": "laravel/laravel",
    "type": "project",
    "description": "The Laravel Framework.",
    "keywords": [
    "framework",
    "laravel"
    ],
    "license": "MIT",
    "require": {
    "php": "^7.2.5",
    "fideloper/proxy": "^4.2",
    "fzaninotto/faker": "^1.9.1",
    "fruitcake/laravel-cors": "^2.0",
    "guzzlehttp/guzzle": "^6.3",
    "laravel/framework": "^7.24",
    "laravel/passport": "^9.3",
    "laravel/tinker": "^2.0",
    "laravel/ui": "^2.2"
    },
    "require-dev": {
    "facade/ignition": "^2.0",
    "mockery/mockery": "^1.3.1",
    "nunomaduro/collision": "^4.1",
    "phpunit/phpunit": "^8.5"
    },
    "config": {
    "optimize-autoloader": true,
    "preferred-install": "dist",
    "sort-packages": true
    },
    "extra": {
    "laravel": {
    "dont-discover": []
    }
    },
    "autoload": {
    "psr-4": {
    "App\\": "app/"
    },
    "classmap": [
    "database/seeds",
    "database/factories"
    ]
    },
    "autoload-dev": {
    "psr-4": {
    "Tests\\": "tests/"
    }
    },
    "minimum-stability": "dev",
    "prefer-stable": true,
    "scripts": {
    "post-autoload-dump": [
    "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
    "@php artisan package:discover --ansi"
    ],
    "post-root-package-install": [
    "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
    ],
    "post-create-project-cmd": [
    "@php artisan key:generate --ansi"
    ],
    "post-install-cmd": [
    "php artisan clear-compiled",
    "chmod -R 777 storage",
    "php artisan passport:keys"
    ]
    }
    }
    view raw composer.json hosted with ❤ by GitHub
  5. Open the project root directory path in a command prompt window or a new terminal window and enter the command below. heroku create

Step 2

  1. With the command prompt or terminal window still opened, enter the following commands. heroku buildpacks:set heroku/php heroku buildpacks:add --index 1 heroku/nodejs

  2. Commit the codes to github and also deploy the code to Heroku using the command below. git push heroku main. If you are pushing from a separate branch, you can do git push heroku :main

  3. Enter the code below after you have verified everything worked correctly and enter the code below. heroku addons:create heroku-postgresql:hobby-dev

  4. Login to your Heroku dashboard from your browser and open up the last app that was created. See the screenshot below as a guide.

    laravel vue heroku full stack

  5. After clicking on the link in the highlighted section, you’ll be redirected to another page. The page contains your database configurations. See the screenshot below as a guide.

    laravel vue heroku full stack

  6. The next step is to load our .env files into Heroku. You can do that by using the command below in your terminal window or command prompt.

    heroku config:set APP_NAME=Ld-Fullstack

  7. After you have successfully entered all the config from the .env, enter the command below.

    heroku run php artisan migrate heroku run php artisan db:seed heroku run php artisan passport:install

  8. Login to your Heroku dashboard again and click on the open app button by the top right corner.

Learning Tools

There are a lot of learning tools online. But for this lesson, I utilized the resources below.

  1. Laravel Doc: Laravel Official Website.
  2. Heroku Doc: Heroku Official Website.

Learning Strategy

I used the learning tools above to achieve this with a ton of extra help from Stack Overflow. I got stuck several times while I was preparing this lesson and using Stack Overflow was the best thing I did. You can leave a comment below if you get stuck so we can treat the problem but at the same time, I recommend you use Stack Overflow most of the time.

Reflective Analysis

It was very difficult especially with getting passport to work on heroku but I was able to gain some deeper insights into how Heroku works and programming in general.

Building a Fullstack application still comes in handy although most folks are migrating to building JAMSTACK Apps now but with a Fullstack application, you have all your codebase in one place and it makes updating or adding a new feature a lot easier.

You can also build a fullstack application with other technologies as well. Such as one with Laravel & PHP, Nodejs & Vuejs, Nodejs & Reactjs, and so much more.

While I was creating this lecture, I didn’t make provision for the logout area but I believe you should be able to make that work. I have also provided you with the link to the active project and also the link to the development and test branch on github.

Conclusion

This project mainly focuses on building a Fullstack application with Laravel & Vuejs but you can always build a Fullstack application with any technology provided you have good knowledge of how the technology works.

However, there are a few other combinations such as Nodejs & Vuejs or Nodejs & Reactjs you can try especially if you are from a javascript background. But I usually go with Laravel because it is very easy to set up and work with.

As a solo developer, you will find yourself building Fullstack applications more often because just as I said, it is easier to work with and it is also very easy to maintain.

Get the complete project from github Test The Live Demo On Heroku.