Subir imágenes

Agregar campo ‘image’ a la migración en database/migrations/2022_02_13_172445_create_posts_table.php:

public function up()
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();

        $table->string('title');
        $table->string('content');
        $table->string('image');

        $table->timestamps();
    });
}

filesystems Cambiar el path de acceso de local a public en config/filesystems.php:

'default' => env('FILESYSTEM_DRIVER', 'public'),

.env Asegurarnos que también este en .env:

FILESYSTEM_DISK=public 

PostFactory Crear imágenes falsas en database/factories/PostFactory.php:

class PostFactory extends Factory
{
    public function definition()
    {
        return [
            'title' => $this->faker->sentence(),
            'content' => $this->faker->text(),
            'image' => 'posts-images/' . $this->faker->image('public/storage/posts-images', 640, 480, null, false), // con false me regresa solo: imagen.jpg
        ];
    }
}

DatabaseSeeder Crear la carpeta posts-images/ en database/seeders/DatabaseSeeder.php:

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Storage;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        // Crear la carpeta donde se almacenaran las imágenes
        Storage::deleteDirectory('posts-images');
        Storage::makeDirectory('posts-images');

        \App\Models\Post::factory(100)->create();
    }
}

Ahora Storage Link con lando!

lando php artisan storage:link

Ahora Ejecutar los Seeders migrate:fresh:

lando php artisan migrate:fresh --seed

Listo! Volver a registrarnos en el sistema.

Para poder subir y ver imágenes en el componente de livewire en app/Http/Livewire/CreatePost.php:

use Livewire\WithFileUploads;
class CreatePost extends Component
{
    // Para poder usar imágenes en Livewire
    use WithFileUploads;

    public $open = false;
    public $title, $content, $image;
    ...
}

Agregamos de una vez la propiedad $image

Crear el input de image y sincronizarla con la propiedad $image resources/views/livewire/create-post.blade.php

{{-- Imagen del post --}}
<div>
    <input type="file" wire:model="image">

</div> 

agregar image a las reglas de validación para que sea requerido, qeu sea una imagen y que máximo tenga 2MB, en app/Http/Livewire/CreatePost.php

protected $rules = [
    'title' => 'required',
    'content' => 'required',
    'image' => 'required|image|max:2048', //image max 2Mb
]; 

Alertas de tailwind https://v1.tailwindcss.com/components/alerts

{{-- tailwind alert --}}
<div class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
    <strong class="font-bold">Holy smokes!</strong>
    <span class="block sm:inline">Something seriously bad happened.</span>
    <span class="absolute top-0 bottom-0 right-0 px-4 py-3">
        <svg class="fill-current h-6 w-6 text-red-500" role="button" xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 20 20">
            <title>Close</title>
            <path
                d="M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z" />
        </svg>
    </span>
</div>

Agregar el alert en resources/views/livewire/create-post.blade.php:

 <x-slot name='content'>

    {{-- tailwind alert --}}
    <div wire:loading wire:target='image' class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
        <strong class="font-bold">Cargando imagen!</strong><span class="block sm:inline">Espere un momento hasta que la imagen se haya procesado.</span>
    </div>

    @if ($image)
        <img class="mb-4" src="{{ $image->temporaryUrl() }}">
        {{ sleep(1) }}
    @endif

    ...

Tambien deshabilidar boton de crear post cuando se este cargando una imagen, en resources/views/livewire/create-post.blade.php:

<x-slot name='footer'>
    <x-jet-secondary-button class="mr-2" wire:click="$set('open', false)">
        Cancelar
    </x-jet-secondary-button>

    <x-jet-danger-button wire:click="save" wire:loading.attr="disabled" wire:target='save, image'
        class="disabled:opacity-25">
        Crear Post
    </x-jet-danger-button>
</x-slot>

Listo!

CreatePost Para salvar la imagen. Código completo en app/Http/Livewire/CreatePost.php:

<?php

namespace App\Http\Livewire;

use App\Models\Post;
use Livewire\Component;
use Livewire\WithFileUploads;

class CreatePost extends Component
{
    use WithFileUploads;

    public $open = true;
    public $title, $content, $image, $identificador;

    protected $rules = [
        'title' => 'required',
        'content' => 'required',
        'image' => 'required|image|max:2048', //image max 2Mb
    ];

    public function render(){
        return view('livewire.create-post');
    }

    public function mount(){
        $this->identificador = rand(); //init con numero al azar
    }

    public function save(){
        sleep(1);
        $this->validate();

        // guardar la imagen en carpeta public/posts
        // $image_url = $this->image->store('posts');
        // guardar la imagen en carpeta public/posts-images
        // $image_url = $this->image->store('public/posts-images');

        // guardar la imagen en carpeta storage/app/public/posts-images/
        $image_url = $this->image->store('posts-images');


        // Agregar registro a la tabla posts
        Post::create([
            'title' => $this->title,
            'content' => $this->content,
            'image' => $image_url,
        ]);

        $this->reset(['open','title','content','image']);
        $this->identificador = rand(); //init con numero al azar

        $this->emitTo('show-posts','renderiza');
        $this->emit('alerta', 'El Post se creó satisfactoriamente');
    }


    // se ejecuta cada vez que cambia una de las propiedades title or content
    // public function updated($propertyName)
    // {
    //     // cada vez que se da una letra checa si cumple con las reglas de validación
    //     $this->validateOnly($propertyName);
    // }

}

En app/Models/Post.php:

protected $fillable = [
    'title',
    'content',
    'image',
];

create-post Código completo en resources/views/livewire/create-post.blade.php:

<div>
    {{-- Do your work, then step back. --}}
    <x-jet-danger-button wire:click="$set('open', true)">
        Crear post
    </x-jet-danger-button>

    <x-jet-dialog-modal wire:model='open'>

        <x-slot name='title'>
            Crear nuevo post
        </x-slot>

        <x-slot name='content'>

            {{-- tailwind alert, para avisar al user que se esta subiendo una imagen --}}
            <div wire:loading wire:target='image' class="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative" role="alert">
                <strong class="font-bold">¡Cargando imagen!</strong>
                <span class="sm:inline">Espere un momento ...</span>
                {{-- con block lo baja una linea --}}
                {{-- <span class="block sm:inline">Espere un momento, en lo que procesa ...</span>  --}}
            </div>

            {{-- Cada vez que se selecciona una imagen en el input de input type="file", se almacena una imagen temporal en temporaryUrl() que se encuentra real en path /storage/app/livewire-tmp  --}}
            @if ($image)
                <img class="mb-4" src="{{ $image->temporaryUrl() }}">
                {{-- use sleep(1) un segundo para simular tiempo de espera de internet --}}
                {{ sleep(1) }} 
            @endif

            {{-- Titulo del post --}}
            <div class="mb-4">
                <x-jet-label value="Título del Post"></x-jet-label>
                <x-jet-input type="text" class="w-full" wire:model="title"></x-jet-input>
                <x-jet-input-error for='title' />
            </div>

            {{-- Contenido del post --}}
            <div class="mb-4">
                <x-jet-label value="Contenido del Post"></x-jet-label>
                <textarea wire:model.defer="content" class="form-control w-full" rows="6"></textarea>
                <x-jet-input-error for='content' />
            </div>


            {{-- imagen del post --}}
            <div>
                <input type="file" wire:model='image' id="{{ $identificador }}">
                <x-jet-input-error for='image' />
            </div>
             {{-- Cualquier imagen que seleccionemos en este input sera almacenada en la propiedad image, gracias al wire:model='image' --}}

        </x-slot>

        <x-slot name='footer'>
            <x-jet-secondary-button class="mr-2" wire:click="$set('open', false)">
                Cancelar
            </x-jet-secondary-button>

            // deshabilita botón "Crear Post" cuando se este salvando y/o subiendo imagen
            <x-jet-danger-button wire:click="save" wire:loading.attr="disabled" wire:target='save, image'
                class="disabled:opacity-25">
                Crear Post
            </x-jet-danger-button>

            {{-- solo mostrar span cuando se esta ejecutando el método save --}}
            {{-- <span wire:loading wire:target='save'>Cargando ...</span> --}}
        </x-slot>

    </x-jet-dialog-modal>

</div>

Nota: Deshabilita botón “Crear Post” cuando se este salvando y/o subiendo imagen. Esto lo logramos con:

wire:target='save, image' 

No olvidar dar de Alta image a la asignación masiva en: app/Models/Post.php

protected $fillable = ['title', 'content', 'image']; 

Listo!

usar $identificador para que haga reset a la variable file del input de la imagen y asi ya no ver el ultimo nombre de archivo que subimos. Listo!