Laravel for Beginners #4 - Create a Dashboard
Tags: laravel, php, webdev, tutorial
Published: 2023-04-03 14:55:00

> [Download source code here.](https://www.ericsdevblog.com/posts/laravel-for-beginners-1?ref=Dev_Community/) ⬅️

Finally, it is time for the most important part of this tutorial. Let's create a fully featured blog application, with posts, categories, as well as tags. Previously, we discussed the CRUD operations for posts, and in this article, we are going to repeat that for categories and tags, and we are also going to discuss how to deal with the relations between them as well.

## Initialize a new Laravel project

Once again, we'll start with a fresh project. Create a working directory and change into it. Make sure Docker is up and running, then execute the following command:

```cmd
curl -s https://laravel.build/<app_name> | bash
```

Change into the app directory and start the server.

```cmd
cd <app_name>
```

```cmd
./vendor/bin/sail up
```

To make things easier, let's create an alias for `sail`. Run the following command:

```cmd
alias sail='[ -f sail ] && sh sail || sh vendor/bin/sail'
```

From now on, you can run sail directly without specifying the entire path.

```cmd
sail up
```

## User authentication

Step two, as we've mentioned before, Laravel comes with a large ecosystem, and Laravel Breeze is a part of this ecosystem. It provides a quick way to set up user authentication and registration in a Laravel application.

Breeze includes pre-built authentication views and controllers, as well as a set of backend APIs for handling user authentication and registration. The package is designed to be easy to install and configure, with minimal setup required.

Use the following commands to install Laravel Breeze:

```cmd
sail composer require laravel/breeze --dev
```

```cmd
sail artisan breeze:install
```

```cmd
sail artisan migrate
```

```cmd
sail npm install
```

```cmd
sail npm run dev
```

This process will automatically generate required controllers, middleware and views that are necessary for creating a basic user authentication system. You may access the registration page by visiting `http://127.0.0.1/register`.

![Laravel Breeze register](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3pfrxif3jrdm03g4ajwi.png)

Register a new account and you will be redirected to the dashboard.

In this article, we are not going to discuss exactly how this user authentication system works, as it is related to some rather advanced concepts. But it is highly recommended that you take a look at the generated files, they offer you a deeper insight into how things work in Laravel.

## Set up database

Next, we need to have a big picture on how our blog app looks. First, we need to have a database that can store posts, categories, and tags. Each database table would have the following structure:

**Posts**

| key          | type       |
| ------------ | ---------- |
| id           | bigInteger |
| created_at   |            |
| updated_at   |            |
| title        | string     |
| cover        | string     |
| content      | text       |
| is_published | boolean    |

**Categories**

| key        | type       |
| ---------- | ---------- |
| id         | bigInteger |
| created_at |            |
| updated_at |            |
| name       | string     |

**Tags**

| key        | type       |
| ---------- | ---------- |
| id         | bigInteger |
| created_at |            |
| updated_at |            |
| name       | string     |

And of course, there should also be a users table, but it has already been generated for us by Laravel Breeze, so we'll skip it this time.

These tables also have relations with each other, as shown in the list below:

- Each user has multiple posts
- Each category has many posts
- Each tag has many posts
- Each post belongs to one user
- Each post belongs to one category
- Each post has many tags

To create these relations, we must modify the posts table:

**Posts with relations**

| key          | type       |
| ------------ | ---------- |
| id           | bigInteger |
| created_at   |            |
| updated_at   |            |
| title        | string     |
| cover        | string     |
| content      | text       |
| is_published | boolean    |
| user_id      | bigInteger |
| category_id  | bigInteger |

And we also need a separate table for post/tag relation:

**Post/tag**

| key     | type       |
| ------- | ---------- |
| post_id | bigInteger |
| tag_id  | bigInteger |

### Implement database structure

To implement this design, generate models and migration files using the following commands:

```cmd
sail artisan make:model Post --migration
```

```cmd
sail artisan make:model Category --migration
```

```cmd
sail artisan make:model Tag --migration
```

And a separate migration file for the `post_tag` table:

```cmd
sail artisan make:migration create_post_tag_table
```

`database/migrations/create_posts_table.php`

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('title');
            $table->string('cover');
            $table->text('content');
            $table->boolean('is_published');

            $table->bigInteger('user_id');
            $table->bigInteger('category_id');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
```

`database/migrations/create_categories_table.php`

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('name');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};
```

`database/migrations/create_tags_table.php`

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('name');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('tags');
    }
};
```

`database/migrations/create_post_tag_table.php`

```php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->bigInteger('post_id');
            $table->bigInteger('tag_id');
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('post_tag');
    }
};
```

Apply these changes with the following command:

```cmd
sail artisan migrate
```

And then for the corresponding models, we need to enable [mass assignment](https://laravel.com/docs/10.x/eloquent#mass-assignment) for selected fields so that we may use `create` or `update` methods on them, as we've discussed in [previous tutorials](https://www.ericsdevblog.com/posts/laravel-for-beginners-2/#inserting--updating-model). And we also need to define relations between database tables.

`app/Models/Post.php`

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        "title",
        'content',
        'cover',
        'is_published'
    ];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }

    public function category(): BelongsTo
    {
        return $this->belongsTo(Category::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }
}

```

`app/Models/Category.php`

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

class Category extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
    ];

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

```

`app/Models/Tag.php`

```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Tag extends Model
{
    use HasFactory;

    protected $fillable = [
        'name',
    ];

    public function posts(): BelongsToMany
    {
        return $this->belongsToMany(Post::class);
    }
}

```

`app/Models/User.php`

```php
<?php

namespace App\Models;

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function posts(): HasMany
    {
        return $this->hasMany(Post::class);
    }
}

```

## Controllers and routes

As for the controllers, we need one [resource controller](https://www.ericsdevblog.com/posts/laravel-for-beginners-2/#resource-controller) for each resources (post, category and tag).

```cmd
php artisan make:controller PostController --resource
```

```cmd
php artisan make:controller CategoryController --resource
```

```cmd
php artisan make:controller TagController --resource
```

Then create routes for each of these controllers:

`routes/web.php`

```php
<?php

use App\Http\Controllers\CategoryController;
use App\Http\Controllers\PostController;
use App\Http\Controllers\ProfileController;
use App\Http\Controllers\TagController;
use Illuminate\Support\Facades\Route;

// Dashboard routes
Route::prefix('dashboard')->group(function () {

    // Dashboard homepage
    Route::get('/', function () {
        return view('dashboard');
    })->name('dashboard');

    // Dashboard category resource
    Route::resource('categories', CategoryController::class);

    // Dashboard tag resource
    Route::resource('tags', TagController::class);

    // Dashboard post resource
    Route::resource('posts', PostController::class);
    
})->middleware(['auth', 'verified']);

Route::middleware('auth')->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
    Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
    Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
});

require __DIR__ . '/auth.php';
```

Notice that all the routes are grouped with a `/dashboard` prefix, and the group has a middleware `auth`, meaning that the user must be logged in to access the dashboard.

### Category/tag controllers

The `CategoryController` and the `TagController` are fairly straightforward. You can set them up the same way we created the `PostController` in the [previous article](https://www.ericsdevblog.com/posts/laravel-for-beginners-3?ref=Dev_Community/).

`app/Http/Controllers/CategoryController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class CategoryController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): View
    {
        $categories = Category::all();

        return view('categories.index', [
            'categories' => $categories
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create(): View
    {
        return view('categories.create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request): RedirectResponse
    {
        // Get the data from the request
        $name = $request->input('name');

        // Create a new Post instance and put the requested data to the corresponding column
        $category = new Category();
        $category->name = $name;

        // Save the data
        $category->save();

        return redirect()->route('categories.index');
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id): View
    {
        $category = Category::all()->find($id);
        $posts = $category->posts();

        return view('categories.show', [
            'category' => $category,
            'posts' => $posts
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id): View
    {
        $category = Category::all()->find($id);

        return view('categories.edit', [
            'category' => $category
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id): RedirectResponse
    {
        // Get the data from the request
        $name = $request->input('name');

        // Find the requested category and put the requested data to the corresponding column
        $category = Category::all()->find($id);
        $category->name = $name;

        // Save the data
        $category->save();

        return redirect()->route('categories.index');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id): RedirectResponse
    {
        $category = Category::all()->find($id);

        $category->delete();

        return redirect()->route('categories.index');
    }
}
```

`app/Http/Controllers/TagController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class TagController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): View
    {
        $tags = Tag::all();

        return view('tags.index', [
            'tags' => $tags
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create(): View
    {
        return view('tags.create');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request): RedirectResponse
    {
        // Get the data from the request
        $name = $request->input('name');

        // Create a new Post instance and put the requested data to the corresponding column
        $tag = new Tag();
        $tag->name = $name;

        // Save the data
        $tag->save();

        return redirect()->route('tags.index');
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id): View
    {
        $tag = Tag::all()->find($id);
        $posts = $tag->posts();

        return view('tags.show', [
            'tag' => $tag,
            'posts' => $posts
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id): View
    {
        $tag = Tag::all()->find($id);

        return view('tags.edit', [
            'tag' => $tag
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id): RedirectResponse
    {
        // Get the data from the request
        $name = $request->input('name');

        // Find the requested category and put the requested data to the corresponding column
        $tag = Tag::all()->find($id);
        $tag->name = $name;

        // Save the data
        $tag->save();

        return redirect()->route('tags.index');
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id): RedirectResponse
    {
        $tag = Tag::all()->find($id);

        $tag->delete();

        return redirect()->route('tags.index');
    }
}
```

Remember that you can check the name of the routes using the following command:

```cmd
sail artisan route:list
```

### Post controller

The PostController, on the other hand, is a bit more complicated, since you have to deal with image uploads and relations in the `store()` method. Let's take a closer look:

`app/Http/Controllers/PostController.php`

```php
<?php

namespace App\Http\Controllers;

use App\Models\Category;
use App\Models\Post;
use App\Models\Tag;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Auth;
use Illuminate\Database\Eloquent\Builder;

class PostController extends Controller
{
    . . .

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request): RedirectResponse
    {
        // Get the data from the request
        $title = $request->input('title');
        $content = $request->input('content');

        if ($request->input('is_published') == 'on') {
            $is_published = true;
        } else {
            $is_published = false;
        }

        // Create a new Post instance and put the requested data to the corresponding column
        $post = new Post();
        $post->title = $title;
        $post->content = $content;
        $post->is_published = $is_published;

        // Save the cover image
        $path = $request->file('cover')->store('cover', 'public');
        $post->cover = $path;

        // Set user
        $user = Auth::user();
        $post->user()->associate($user);

        // Set category
        $category = Category::find($request->input('category'));
        $post->category()->associate($category);

        // Save post
        $post->save();

        //Set tags
        $tags = $request->input('tags');

        foreach ($tags as $tag) {
            $post->tags()->attach($tag);
        }

        return redirect()->route('posts.index');
    }

    . . .
}
```

A few things to be noted in this `store()` method. First, line 28 to 32, we are going to use an HTML checkbox to represent the `is_published` field, and its values are either `'on'` or `null`. But in the database, its values are saved as `true` or `false`, so we must use an `if` statement to solve this issue.

Line 41 to 42, to retrieve files, we must use the `file()` method instead of `input()`, and the file is saved in the `public` disk under directory `cover`.

Line 45 to 46, get the current user using `Auth::user()`, and associate the post with the user using `associate()` method. And line 49 to 50 does the same thing for category. Remember you can only do this from `$post` and not `$user` or `$category`, since the `user_id` and `category_id` columns are in the `posts` table.

Lastly, for the tags, as demonstrated from line 56 to 60, you must save the current post to the database, and then retrieve a list of tags, and attach each of them to the post one by one, using the `attach()` method.

For the `update()` method, things work similarly, except that you must remove all existing tags before you can attach the new ones.

```php
$post->tags()->detach();
```

## Views

When building a view system, always remember to be organized. This is the structure I'm going with:

```text
resources/views
├── auth
├── categories
│   ├── create.blade.php
│   ├── edit.blade.php
│   ├── index.blade.php
│   └── show.blade.php
├── components
├── layouts
├── posts
│   ├── create.blade.php
│   ├── edit.blade.php
│   ├── index.blade.php
│   └── show.blade.php
├── profile
├── tags
│   ├── create.blade.php
│   ├── edit.blade.php
│   ├── index.blade.php
│   └── show.blade.php
├── dashboard.blade.php
└── welcome.blade.php
```

I've created three directories, `posts`, `categories` and `tags`, and each of them has four templates, `create`, `edit`, `index` and `show` (except for `posts` since it is unnecessary to have a `show` page for posts in the dashboard).

Including all of these views in one article would make this tutorial unnecessarily long, so instead, i'm only going to demonstrate the create, edit and index pages for posts. However, the source code for this tutorial is available for free [here](https://github.com/ericsdevblog/laravel-tutorial), if you need some reference.

### Post create view

`resources/views/posts/create.blade.php`

```html
<x-app-layout>
    <x-slot name="header">
        <div class="flex justify-between">
            <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
                {{ __('Posts') }}
            </h2>
            <a href="{{ route('posts.create') }}">
                <x-primary-button>{{ __('New') }}</x-primary-button>
            </a>
        </div>

        <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin"></script>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
                <div class="">
                    <form action="{{ route('posts.store') }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data">
                        {{ csrf_field() }}
                        <input type="checkbox" name="is_published" id="is_published">
                        <x-input-label for="is_published">Make this post public</x-input-label>
                        <br>
                        <x-input-label for="title">{{ __('Title') }}</x-input-label>
                        <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" />
                        <br>
                        <x-input-label for="content">{{ __('Content') }}</x-input-label>
                        <textarea name="content" id="content" cols="30" rows="30"></textarea>
                        <br>
                        <x-input-label for="cover">{{ __('Cover Image') }}</x-input-label>
                        <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" required autofocus autocomplete="cover" />
                        <br>
                        <x-input-label for="category">{{ __('Category') }}</x-input-label>
                        <select id="category" name="category">
                            @foreach($categories as $category)
                            <option value="{{ $category->id }}">{{ $category->name }}</option>
                            @endforeach
                        </select>
                        <br>
                        <x-input-label for="tags">{{ __('Tags') }}</x-input-label>
                        <select id="tags" name="tags[]" multiple>
                            @foreach($tags as $tag)
                            <option value="{{ $tag->id }}">{{ $tag->name }}</option>
                            @endforeach
                        </select>
                        <br>
                        <x-primary-button>{{ __('Save') }}</x-primary-button>
                    </form>
                    <script>
                        tinymce.init({. . .});
                    </script>
                </div>
            </div>

        </div>
    </div>
</x-app-layout>
```

I'm using [TinyMCE](https://www.tiny.cloud/) as the rich text editor, you can replace it with something else, or simply use a `<textarea></textarea>` if you wish.

Line 19, this form must have `enctype="multipart/form-data"` since we are not just transferring texts, there are files as well.

Line 31, remember to use `type="file"` here since we are uploading an image.

Line 34 to 38, the value of the option will be transferred to the backend.

Line 41 to 45, there are two things you must pay attention to here. First, notice `name="tags[]"`, the `[]` tells Laravel to transfer an iterable array instead of texts. And second, `multiple` creates a multi-select form instead of single select like the one for categories.

### Post edit view

`resources/views/posts/edit.blade.php`

```html
<x-app-layout>
    <x-slot name="header">
        <div class="flex justify-between">
            <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
                {{ __('Posts') }}
            </h2>
            <a href="{{ route('posts.create') }}">
                <x-primary-button>{{ __('New') }}</x-primary-button>
            </a>
        </div>

        <script src="https://cdn.tiny.cloud/. . ./tinymce.min.js" referrerpolicy="origin"></script>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
                <div class="">
                    <form action="{{ route('posts.update', ['post' => $post->id]) }}" method="POST" class="mt-6 space-y-3" enctype="multipart/form-data">
                        {{ csrf_field() }}
                        {{ method_field('PUT') }}
                        <input type="checkbox" name="is_published" id="is_published" @checked($post->is_published)/>
                        <x-input-label for="is_published">Make this post public</x-input-label>
                        <br>
                        <x-input-label for="title">{{ __('Title') }}</x-input-label>
                        <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" required autofocus autocomplete="name" value="{{ $post->title }}" />
                        <br>
                        <x-input-label for="content">{{ __('Content') }}</x-input-label>
                        <textarea name="content" id="content" cols="30" rows="30">{{ $post->content }}</textarea>
                        <br>
                        <x-input-label for="cover">{{ __('Update Cover Image') }}</x-input-label>
                        <img src="{{ Illuminate\Support\Facades\Storage::url($post->cover) }}" alt="cover image" width="200">
                        <x-text-input id="cover" name="cover" type="file" class="mt-1 block w-full" autofocus autocomplete="cover" />
                        <br>
                        <x-input-label for="category">{{ __('Category') }}</x-input-label>
                        <select id="category" name="category">
                            @foreach($categories as $category)
                            <option value="{{ $category->id }}" @selected($post->category->id == $category->id)>{{ $category->name }}</option>
                            @endforeach
                        </select>
                        <br>
                        <x-input-label for="tags">{{ __('Tags') }}</x-input-label>
                        <select id="tags" name="tags[]" multiple>
                            @foreach($tags as $tag)
                            <option value="{{ $tag->id }}" @selected($post->tags->contains($tag))>{{ $tag->name }}</option>
                            @endforeach
                        </select>
                        <br>
                        <x-primary-button>{{ __('Save') }}</x-primary-button>
                    </form>
                    <script>
                        tinymce.init({. . .});
                    </script>
                </div>
            </div>

        </div>
    </div>
</x-app-layout>
```

Line 19 to 21, by default, HTML doesn't support PUT method, so what we can do is use `method="POST"`, and then tell Laravel to use PUT method with `{{ method_field('PUT') }}`.

### Post index view

`resources/views/posts/index.blade.php`

```html
<x-app-layout>
    <x-slot name="header">
        <div class="flex justify-between">
            <h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
                {{ __('Posts') }}
            </h2>
            <a href="{{ route('posts.create') }}">
                <x-primary-button>{{ __('New') }}</x-primary-button>
            </a>
        </div>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            @foreach($posts as $post)
            <div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg mb-4 px-4 h-20 flex justify-between items-center">
                <div class="text-gray-900 dark:text-gray-100">
                    <p>{{ $post->title }}</p>
                </div>
                <div class="space-x-2">
                    <a href="{{ route('posts.edit', ['post' => $post->id]) }}"> <x-primary-button>{{ __('Edit') }}</x-primary-button></a>
                    <form method="post" action="{{ route('posts.destroy', ['post' => $post->id]) }}" class="inline">
                        {{ csrf_field() }}
                        {{ method_field('DELETE') }}
                        <x-danger-button>
                            {{ __('Delete') }}
                        </x-danger-button>
                    </form>
                </div>
            </div>
            @endforeach
        </div>
    </div>
</x-app-layout>
```

Notice the delete button, instead of a regular button, it must be a form with DELETE method, since a regular link has only GET method.

With this demonstration, you should be able to build the rest of the view system with ease.

### Screenshots

Last but not least, here are some screenshots for the dashboard I've created.

![Dashboard home](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/udftqcun4iuhl8obubry.png)

![Categories list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w134slztmzxocor4iouh.png)

![Create category](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2oes3dj3la16nqy4tz7p.png)

![Update post](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yjixsgfgn5hybkch591x.jpeg)

In the next article, we are going to move on from the dashboard, and create the frontend part of the application the user can see.

> If you liked this article, please also check out my other tutorials:
> - [Django for Beginners](https://www.ericsdevblog.com/posts/django-for-beginners-1?ref=Dev_Community/)
> - [Vue.js for Beginners](https://www.ericsdevblog.com/posts/vuejs-for-beginners-1?ref=Dev_Community/)
> - [Create a Modern Application with Django and Vue](https://www.ericsdevblog.com/posts/create-a-modern-application-with-django-and-vue-1?ref=Dev_Community/)
> - [Beginner’s Roadmap to Web Development](https://www.ericsdevblog.com/series/beginners-roadmap-to-web-development?ref=Dev_Community/)