Laravel 10 CRUD and Image Upload Tutorial with Laravel Breeze and Repo Example
Tags: php, laravel, authentication, crud
Published: 2023-02-18 02:43:27

---
title: Laravel 10 CRUD and Image Upload Tutorial with Laravel Breeze and Repo Example
published: true
date: 2023-02-16 00:04:00 UTC
tags: 
- PHP
- Laravel
- Authentication
- CRUD
canonical_url: https://fajarwz.com/blog/laravel-10-crud-and-image-upload-tutorial-with-laravel-breeze-and-repo-example/
---

![Laravel 10 CRUD and image upload illustration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/93ojubue8reyp1ytlvqb.png)

> Build a Laravel 10 CRUD App with Image Upload using Breeze - Step-by-Step Tutorial with Repo Example

Laravel is a popular open-source PHP framework that makes it easy to build web applications. It provides a powerful set of tools and features for web developers, such as routing, middleware, database integration, and templating.

Laravel 10 is the latest version of Laravel, released in 2023. It comes with several new features and improvements, such as support for PHP 8.1, enhanced security features, and performance optimizations.

In this tutorial, we will create a simple CRUD (Create, Read, Update, Delete) application using Laravel, one of the most popular PHP frameworks. The CRUD application will allow us to manage a list of blog posts, including creating new posts, reading existing posts, updating posts, and deleting posts. In addition to that, we will also learn how to upload and manage images for the blog posts.

By the end of this tutorial, you will have a good understanding of how to build a basic CRUD application in Laravel and how to work with images in the application. Let's get started!

## Install Laravel 10
Before we begin, make sure you have the latest version of PHP (at least PHP 8.1) and Composer installed on your system. To install Laravel 10, you can use the following command:

```
composer create-project --prefer-dist laravel/laravel:^10 pmid-laravel10-breeze
```

This command will create a new Laravel 10 project in a directory named pmid-laravel10-breeze.

## Initializing the Database
Next, we need to set up a database. For this tutorial, create a database named `pmid_laravel10_breeze`. After creating the database, we need to modify the database connection settings in our Laravel app's `.env` file to match the new database name, as well as the username and password.

## Install Laravel Breeze
Laravel Breeze is a lightweight authentication scaffold for Laravel 8 and later versions. It provides a simple way to add authentication functionality to your Laravel application.

To install Laravel Breeze, run the following command in your terminal:

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

After downloading Laravel Breeze via Composer, we can run the `breeze:install` Artisan command to install Laravel Breeze in our Laravel 10 application:

```
php artisan breeze:install
```

A prompt will appear, asking which stack we want to install. Choose "blade". Another prompt may appear asking if we want to enable dark mode support. For now, we won't use it, so answer "no". Finally, a prompt may appear asking if we want to use the Pest testing framework. Currently, we won't use it, so answer "no".

## Create Posts Migration
Now that we have installed Laravel 10 and Laravel Breeze, let's create a migration for our `posts` table. This table will store our post data for this CRUD tutorial.

To create a migration, run the following command in your terminal:

```
php artisan make:migration "create posts table"
```

This command will create a new migration file in the `database/migrations` directory.

Open the migration file and add the following code to create the `posts` table:

```php
public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('content');
        $table->text('featured_image');
        $table->timestamps();
    });
}

public function down()
{
    Schema::dropIfExists('posts');
}
```

This code will create a posts table with an `id`, `title`, `content`, `featured_image`, and timestamps columns.

## Migrate the Migration
Finally, let's run the migration to create the `posts` table in our database. To do this, run the following command in your terminal:

```
php artisan migrate
```

This command will run all the outstanding migrations in the `database/migrations` directory.

## Create Routes
Now that we have our `posts` table and database set up, let's create some routes to handle the CRUD operations for our blog posts.

Open the `routes/web.php` file and update it with the following code:

```php
<?php

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

Route::get('/', function () {
    return view('welcome');
});

Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');

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');

    // Add the following route to the existing routes because we want the posts route accessible to authenticated users only.
    // We'll use a resource route because it contains all the exact routes we need for a typical CRUD application.
    Route::resource('posts', PostController::class);
});

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

This adds the **posts resource route** to the existing routes and applies the auth middleware to restrict access to authenticated users only. The `PostController` is set up as a resource controller to handle all the typical CRUD operations.

> **Read also:**
> * [Easy Laravel Localization Tutorial With Blog Use Case and Repo Example](https://fajarwz.com/blog/easy-laravel-localization-tutorial-with-blog-use-case-and-repo-example/)
> * [Laravel Export/Import: Step-by-Step Guide with Repo Example](https://fajarwz.com/blog/laravel-export-import-step-by-step-guide-with-repo-example/)

## Create PostController
Now let's create a PostController to handle the CRUD operations for our blog posts. To create a new controller with resource methods, run the following command in your terminal:

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

This command will create a new `PostController` with all the resource methods in the `app/Http/Controllers` directory.

Open the `PostController` and add the following code to the class:

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Storage;
// Use the Post Model
use App\Models\Post;
// We will use Form Request to validate incoming requests from our store and update method
use App\Http\Requests\Post\StoreRequest;
use App\Http\Requests\Post\UpdateRequest;

class PostController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): Response
    {
        return response()->view('posts.index', [
            'posts' => Post::orderBy('updated_at', 'desc')->get(),
        ]);
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create(): Response
    {
        return response()->view('posts.form');
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(StoreRequest $request): RedirectResponse
    {
        $validated = $request->validated();

        if ($request->hasFile('featured_image')) {
            // put image in the public storage
            $file = Storage::disk('public')->put('images/posts/featured-images', request()->file('featured_image'), 'public');
            // get the image path in the url
            $path = Storage::url($file);
            $validated['featured_image'] = $path;
        }

        // insert only requests that already validated in the StoreRequest
        $create = Post::create($validated);

        if($create) {
            // add flash for the success notification
            session()->flash('notif.success', 'Post created successfully!');
            return redirect()->route('posts.index');
        }

        return abort(500);
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id): Response
    {
        return response()->view('posts.show', [
            'post' => Post::findOrFail($id),
        ]);
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id): Response
    {
        return response()->view('posts.form', [
            'post' => Post::findOrFail($id),
        ]);
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(UpdateRequest $request, string $id): RedirectResponse
    {
        $post = Post::findOrFail($id);
        $validated = $request->validated();

        if ($request->hasFile('featured_image')) {
            // get current image path and replace the storage path with public path
            $currentImage = str_replace('/storage', '/public', $post->featured_image);
            // delete current image
            Storage::delete($currentImage);

            $file = Storage::disk('public')->put('images/posts/featured-images', request()->file('featured_image'), 'public');
            $path = Storage::url($file);
            $validated['featured_image'] = $path;
        }

        $update = $post->update($validated);

        if($update) {
            session()->flash('notif.success', 'Post updated successfully!');
            return redirect()->route('posts.index');
        }

        return abort(500);
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id): RedirectResponse
    {
        $post = Post::findOrFail($id);

        $currentImage = str_replace('/storage', '/public', $post->featured_image);
        Storage::delete($currentImage);
        
        $delete = $post->delete($id);

        if($delete) {
            session()->flash('notif.success', 'Post deleted successfully!');
            return redirect()->route('posts.index');
        }

        return abort(500);
    }
}
```

## Create Form Request for store() method
We can run the following command to create a form request for `store()`:

```
php artisan make:request Post/StoreRequest
```

And then add the following code to the class:

```php
    public function authorize(): bool
    {
        // dont' forget to set this as true
        return true;
    }

    public function rules(): array
    {
        // make all of the fields required, set featured image to accept only images
        return [
            'title' => 'required|string|min:3|max:250',
            'content' => 'required|string|min:3|max:6000',
            'featured_image' => 'required|image|max:1024|mimes:jpg,jpeg,png',
        ];
    }
```

## Create Form Request for update() method
We can run the following command to create a form request for `update()`:

```
php artisan make:request Post/UpdateRequest
```

The code for this class is similar to the code for `StoreRequest` except that the `featured_image` rule is set to nullable:

```php
    public function authorize(): bool
    {
        // dont' forget to set this as true
        return true;
    }

    public function rules(): array
    {
        // make all of the fields required, set featured image to accept only images
        return [
            'title' => 'required|string|min:3|max:250',
            'content' => 'required|string|min:3|max:6000',
            'featured_image' => 'nullable|image|max:1024|mimes:jpg,jpeg,png',
        ];
    }
```

## Add $fillable to Post Model
In the `app/Models/Post.php` file, add the fillable property to specify which fields can be mass-assigned. Here's an example:

```php
class Post extends Model
{
    protected $fillable = [
        'title', 
        'content',
        'featured_image',
    ];
}
```

## Create a Symbolic Link for Our Public Storage
To make files stored in the public disk accessible from the web, a symbolic link needs to be created from the `public/storage` folder to the `storage/app/public` folder. By default, the public disk uses the local driver. No changes are required to our public storage configuration. To create the symbolic link, run the following command:

```
php artisan storage:link
```

## TailwindCSS and Vite Configuration 
We will be using TailwindCSS for our styling and Vite as our Asset Bundling helper. Therefore, we need to adjust their configurations a bit.

Since we will be writing additional styles to our app, we need to start the Tailwind CLI build process while we code our views. To do this, run the following command:

```
npx tailwindcss -i ./resources/css/app.css -o ./resources/css/main.css --watch
```

This command will generate a new file at `resources/css/main.css`. Now, we need to tell Vite that we will be using `main.css` instead of `app.css`.

We will reuse `resources/views/layouts/app.blade.php` layout so update the Vite code in the head section to the following:

```blade.php
    {{-- some other code --}}
    @vite(['resources/css/main.css', 'resources/js/app.js'])
</head>
```

## Setting Up the Layout
We will update the header section inside our layout's body so that it can handle flash notifications for success messages. Please update it with the following code:

```blade.php
<!-- Page Heading -->
@if (isset($header))
    <header class="bg-white shadow">
        <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
            {{ $header }}

            {{-- check if there is a notif.success flash session --}}
            @if (Session::has('notif.success'))
            <div class="bg-blue-300 mt-2 p-4">
                {{-- if it's there then print the notification --}}
                <span class="text-white">{{ Session::get('notif.success') }}</span>
            </div>
            @endif
        </div>
    </header>
@endif
```

So our resources\views\layouts\app.blade.php file will be updated with the below code:

```blade.php
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="csrf-token" content="{{ csrf_token() }}">

        <title>{{ config('app.name', 'Laravel') }}</title>

        <!-- Fonts -->
        <link rel="preconnect" href="https://fonts.bunny.net">
        <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />

        <!-- Scripts -->
        @vite(['resources/css/main.css', 'resources/js/app.js'])
    </head>
    <body class="font-sans antialiased">
        <div class="min-h-screen bg-gray-100">
            @include('layouts.navigation')

            <!-- Page Heading -->
            @if (isset($header))
                <header class="bg-white shadow">
                    <div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
                        {{ $header }}

                        @if (Session::has('notif.success'))
                        <div class="bg-blue-300 mt-2 p-4">
                            <span class="text-white">{{ Session::get('notif.success') }}</span>
                        </div>
                        @endif
                    </div>
                </header>
            @endif

            <!-- Page Content -->
            <main>
                {{ $slot }}
            </main>
        </div>
    </body>
</html>
```

> **Read also:**
> * [Information Clarity in Remote Teams: Tips and Examples](https://fajarwz.com/blog/effective-communication-in-remote-teams-tips-and-examples/)
> * [Laravel with(): How to Solve the N+1 Problem with Eager Loading Relationships](https://fajarwz.com/blog/laravel-with-how-to-solve-the-n+1-problem-with-eager-loading-relationships/)

## Create Index View
Create a new file `resources/views/posts/index.blade.php` with the following contents:

```blade.php
{{-- use AppLayout Component located in app\View\Components\AppLayout.php which use resources\views\layouts\app.blade.php view --}}
<x-app-layout>
    <x-slot name="header">
        <div class="flex justify-between">
            <h2 class="font-semibold text-xl text-gray-800 leading-tight">
                {{ 'Posts' }}
            </h2>
            <a href="{{ route('posts.create') }}" class="bg-blue-500 text-white px-4 py-2 rounded-md">ADD</a>
        </div>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                    <table class="border-collapse table-auto w-full text-sm">
                        <thead>
                            <tr>
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Title</th>
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Created At</th>
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Updated At</th>
                                <th class="border-b font-medium p-4 pl-8 pt-0 pb-3 text-slate-400 text-left">Action</th>
                            </tr>
                        </thead>
                        <tbody class="bg-white">
                            {{-- populate our post data --}}
                            @foreach ($posts as $post)
                                <tr>
                                    <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->title }}</td>
                                    <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->created_at }}</td>
                                    <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">{{ $post->updated_at }}</td>
                                    <td class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 text-slate-500 dark:text-slate-400">
                                        <a href="{{ route('posts.show', $post->id) }}" class="border border-blue-500 hover:bg-blue-500 hover:text-white px-4 py-2 rounded-md">SHOW</a>
                                        <a href="{{ route('posts.edit', $post->id) }}" class="border border-yellow-500 hover:bg-yellow-500 hover:text-white px-4 py-2 rounded-md">EDIT</a>
                                        {{-- add delete button using form tag --}}
                                        <form method="post" action="{{ route('posts.destroy', $post->id) }}" class="inline">
                                            @csrf
                                            @method('delete')
                                            <button class="border border-red-500 hover:bg-red-500 hover:text-white px-4 py-2 rounded-md">DELETE</button>
                                        </form>
                                    </td>
                                </tr>
                            @endforeach
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>
```

## Create Show View
Create a new file `resources/views/posts/show.blade.php` with the following contents:

```blade.php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ 'Show' }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                    <div class="mb-6">
                        <h2 class="text-lg font-medium text-gray-900">
                            {{ 'Title' }}
                        </h2>
                
                        <p class="mt-1 text-sm text-gray-600">
                            {{ $post->title }}
                        </p>
                    </div>
                    <div class="mb-6">
                        <h2 class="text-lg font-medium text-gray-900">
                            {{ 'Content' }}
                        </h2>
                
                        <p class="mt-1 text-sm text-gray-600">
                            {{ $post->content }}
                        </p>
                    </div>
                    <div class="mb-6">
                        <h2 class="text-lg font-medium text-gray-900">
                            {{ 'Featured Image' }}
                        </h2>
                
                        <p class="mt-1 text-sm text-gray-600">
                            <img class="h-64 w-128" src="{{ asset($post->featured_image) }}" alt="{{ $post->title }}" srcset="">
                        </p>
                    </div>
                    <div class="mb-6">
                        <h2 class="text-lg font-medium text-gray-900">
                            {{ 'Created At' }}
                        </h2>
                
                        <p class="mt-1 text-sm text-gray-600">
                            {{ $post->created_at }}
                        </p>
                    </div>
                    <div class="mb-6">
                        <h2 class="text-lg font-medium text-gray-900">
                            {{ 'Updated At' }}
                        </h2>
                
                        <p class="mt-1 text-sm text-gray-600">
                            {{ $post->updated_at }}
                        </p>
                    </div>
                    <a href="{{ route('posts.index') }}" class="bg-blue-500 text-white px-4 py-2 rounded-md">BACK</a>
                </div>
            </div>
        </div>
    </div>
</x-app-layout>
```

## Create Form View
Create a new file `resources/views/posts/form.blade.php`. Here we will use some already defined components we get from Laravel Breeze. Write the following code:

```blade.php
<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{-- Use 'Edit' for edit mode and create for non-edit/create mode --}}
            {{ isset($post) ? 'Edit' : 'Create' }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                    {{-- don't forget to add multipart/form-data so we can accept file in our form --}}
                    <form method="post" action="{{ isset($post) ? route('posts.update', $post->id) : route('posts.store') }}" class="mt-6 space-y-6" enctype="multipart/form-data">class="mt-6 space-y-6">
                        @csrf
                        {{-- add @method('put') for edit mode --}}
                        @isset($post)
                            @method('put')
                        @endisset
                
                        <div>
                            <x-input-label for="title" value="Title" />
                            <x-text-input id="title" name="title" type="text" class="mt-1 block w-full" :value="$post->title ?? old('title')" required autofocus />
                            <x-input-error class="mt-2" :messages="$errors->get('title')" />
                        </div>

                        <div>
                            <x-input-label for="content" value="Content" />
                            {{-- use textarea-input component that we will create after this --}}
                            <x-textarea-input id="content" name="content" class="mt-1 block w-full" required autofocus>{{ $post->content ?? old('content') }}</x-textarea-input>
                            <x-input-error class="mt-2" :messages="$errors->get('content')" />
                        </div>

                        <div>
                            <x-input-label for="featured_image" value="Featured Image" />
                            <label class="block mt-2">
                                <span class="sr-only">Choose image</span>
                                <input type="file" id="featured_image" name="featured_image" class="block w-full text-sm text-slate-500
                                    file:mr-4 file:py-2 file:px-4
                                    file:rounded-full file:border-0
                                    file:text-sm file:font-semibold
                                    file:bg-violet-50 file:text-violet-700
                                    hover:file:bg-violet-100
                                "/>
                            </label>
                            <div class="shrink-0 my-2">
                                <img id="featured_image_preview" class="h-64 w-128 object-cover rounded-md" src="{{ isset($post) ? asset($post->featured_image) : '' }}" alt="Featured image preview" />
                            </div>
                            <x-input-error class="mt-2" :messages="$errors->get('featured_image')" />
                        </div>
                
                        <div class="flex items-center gap-4">
                            <x-primary-button>{{ __('Save') }}</x-primary-button>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
    <script>
        // create onchange event listener for featured_image input
        document.getElementById('featured_image').onchange = function(evt) {
            const [file] = this.files
            if (file) {
                // if there is an image, create a preview in featured_image_preview
                document.getElementById('featured_image_preview').src = URL.createObjectURL(file)
            }
        }
    </script>
</x-app-layout>
```

## Create textarea-input Component
Create additional component with the following codes:

```blade.php
@props(['disabled' => false])

<textarea {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>{{$slot}}</textarea>
```

## Update Navigation Component
Update the navigation component with the following codes:

```blade.php
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
    <!-- Primary Navigation Menu -->
    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div class="flex justify-between h-16">
            <div class="flex">
                <!-- Logo -->
                <div class="shrink-0 flex items-center">
                    <a href="{{ route('dashboard') }}">
                        <x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
                    </a>
                </div>

                <!-- Navigation Links -->
                <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
                    <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                        {{ __('Dashboard') }}
                    </x-nav-link>
                    <!-- add this -->
                    <x-nav-link :href="route('posts.index')" :active="request()->routeIs('posts.index')">
                        {{'Posts' }}
                    </x-nav-link>
                </div>
            </div>

            <!-- Settings Dropdown -->
            <div class="hidden sm:flex sm:items-center sm:ml-6">
                <x-dropdown align="right" width="48">
                    <x-slot name="trigger">
                        <button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
                            <div>{{ Auth::user()->name }}</div>

                            <div class="ml-1">
                                <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
                                    <path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
                                </svg>
                            </div>
                        </button>
                    </x-slot>

                    <x-slot name="content">
                        <x-dropdown-link :href="route('profile.edit')">
                            {{ __('Profile') }}
                        </x-dropdown-link>

                        <!-- Authentication -->
                        <form method="POST" action="{{ route('logout') }}">
                            @csrf

                            <x-dropdown-link :href="route('logout')"
                                    onclick="event.preventDefault();
                                                this.closest('form').submit();">
                                {{ __('Log Out') }}
                            </x-dropdown-link>
                        </form>
                    </x-slot>
                </x-dropdown>
            </div>

            <!-- Hamburger -->
            <div class="-mr-2 flex items-center sm:hidden">
                <button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
                    <svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
                        <path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
                        <path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
                    </svg>
                </button>
            </div>
        </div>
    </div>

    <!-- Responsive Navigation Menu -->
    <div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
        <div class="pt-2 pb-3 space-y-1">
            <x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
                {{ __('Dashboard') }}
            </x-responsive-nav-link>
            <!-- add this -->
            <x-responsive-nav-link :href="route('posts.index')" :active="request()->routeIs('posts.index')">
                {{ 'Posts' }}
            </x-responsive-nav-link>
        </div>

        <!-- Responsive Settings Options -->
        <div class="pt-4 pb-1 border-t border-gray-200">
            <div class="px-4">
                <div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
                <div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
            </div>

            <div class="mt-3 space-y-1">
                <x-responsive-nav-link :href="route('profile.edit')">
                    {{ __('Profile') }}
                </x-responsive-nav-link>

                <!-- Authentication -->
                <form method="POST" action="{{ route('logout') }}">
                    @csrf

                    <x-responsive-nav-link :href="route('logout')"
                            onclick="event.preventDefault();
                                        this.closest('form').submit();">
                        {{ __('Log Out') }}
                    </x-responsive-nav-link>
                </form>
            </div>
        </div>
    </div>
</nav>
```

Here we add our **Posts** link to the desktop and mobile version navigation.

## Test Our Laravel App
If you haven't run `npm install` since installing Laravel Breeze, make sure to run it first to install all the necessary dependencies. If you have already done that, you can skip this step. Next, run `npm run dev` to start working with Vite.

Once you have set up your frontend dependencies, you can run your local development server by running the following command:

```
php artisan serve
```

> **Read also:**
> * [Web Rendering: What Is SSR, CSR, SSG, and ISR?](https://fajarwz.com/blog/web-rendering-what-is-ssr-csr-ssg-and-isr/)
> * [Create Login With Google OAuth Using Laravel Socialite](https://fajarwz.com/blog/create-login-with-google-oauth-using-laravel-socialite/)

### Welcome Page
You will see the following Laravel 10 Welcome Page. You will also see **Log in** and **Register** link at the navbar because we already installed Laravel Breeze.

![Welcome page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4lp8t8121dvllzefo5gv.png)

Register yourself and go to the **Dashboard** Page.

### Dashboard Page
Here is how the Dashboard page looks like. You should also see an additional link at the navbar named **Posts**:

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

### Post List Page
Here is how the post list page in `/posts` should look like. It should have a table with **Show**, **Edit**, and **Delete** buttons in each record:

![Post list page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m199qrpv55fay85js6hm.png)

### Show Post Page
Here is how the Show Post page should look like:

![Show post page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jkck6gw9z6pvi7sfag16.png)

### Create and Edit Post Page
Here is how the Create Post page should look like with image preview. This is also similar to the Edit Post page:

![Create and edit post page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2i0c2w9mojncvt7jhi3z.png)

### Notification Display
A notification should appear after we successfully create, edit, or delete a record:

![Notification display](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/09bny7gh3b3syj7ndr6w.png)

## Conclusion
In this tutorial, we have learned how to create a simple CRUD application using Laravel 8 with Laravel Breeze for authentication. We have covered the basic concepts of Laravel, such as routing, controllers, views, and models, as well as using components from Laravel Breeze to handle user authentication.

We also learned how to use Tailwind CSS and Vite to style our application and optimize asset bundling. Additionally, we covered how to handle flash notifications and file uploading in our application.

Overall, this tutorial provides a solid foundation for building CRUD applications using Laravel, and with the knowledge gained, you can easily extend this application to meet your specific requirements.

The repository for this example can be found at [fajarwz/blog-laravel10-crud-image](https://github.com/fajarwz/blog-laravel10-crud-image).