Add User photo media support

This commit is contained in:
Christopher C. Wells 2021-04-21 08:49:05 -07:00
parent f6fa2cf79f
commit 03f7319157
11 changed files with 145 additions and 50 deletions

View File

@ -50,11 +50,28 @@ class UserController extends Controller
*/ */
public function update(UpdateUserRequest $request, User $user): RedirectResponse public function update(UpdateUserRequest $request, User $user): RedirectResponse
{ {
$attributes = $request->validated(); $input = $request->validated();
$attributes['remember_token'] = Str::random(10); $input['remember_token'] = Str::random(10);
$attributes['password'] = Hash::make($attributes['password']); $input['password'] = Hash::make($input['password']);
$attributes['admin'] = $attributes['admin'] ?? false; $input['admin'] = $input['admin'] ?? false;
$user->fill($attributes)->save();
$user->fill($input)->save();
// Handle image.
if (!empty($input['image'])) {
/** @var \Illuminate\Http\UploadedFile $file */
$file = $input['image'];
$user->clearMediaCollection();
$user
->addMediaFromRequest('image')
->usingName($user->username)
->usingFileName("{$user->slug}.{$file->extension()}")
->toMediaCollection();
}
elseif (isset($input['remove_image']) && $input['remove_image']) {
$user->clearMediaCollection();
}
session()->flash('message', "User {$user->name} updated!"); session()->flash('message', "User {$user->name} updated!");
return redirect()->route('users.index'); return redirect()->route('users.index');
} }

View File

@ -19,6 +19,8 @@ class UpdateUserRequest extends FormRequest
'password' => ['nullable', 'string', 'confirmed'], 'password' => ['nullable', 'string', 'confirmed'],
'password_confirmation' => ['nullable', 'string'], 'password_confirmation' => ['nullable', 'string'],
'admin' => ['nullable', 'boolean'], 'admin' => ['nullable', 'boolean'],
'image' => ['nullable', 'file', 'mimes:jpg,png,gif'],
'remove_image' => ['nullable', 'boolean'],
]; ];
if (!$this->user) { if (!$this->user) {
$rules['password'] = ['required', 'string', 'confirmed']; $rules['password'] = ['required', 'string', 'confirmed'];

View File

@ -10,6 +10,9 @@ use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
/** /**
* App\Models\User * App\Models\User
@ -29,6 +32,8 @@ use Illuminate\Support\Facades\Auth;
* @property-read int|null $journal_entries_count * @property-read int|null $journal_entries_count
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications * @property-read \Illuminate\Notifications\DatabaseNotificationCollection|\Illuminate\Notifications\DatabaseNotification[] $notifications
* @property-read int|null $notifications_count * @property-read int|null $notifications_count
* @property-read \Spatie\MediaLibrary\MediaCollections\Models\Collections\MediaCollection|Media[] $media
* @property-read int|null $media_count
* @method static \Database\Factories\UserFactory factory(...$parameters) * @method static \Database\Factories\UserFactory factory(...$parameters)
* @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery() * @method static \Illuminate\Database\Eloquent\Builder|User newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|User newQuery() * @method static \Illuminate\Database\Eloquent\Builder|User newQuery()
@ -46,9 +51,12 @@ use Illuminate\Support\Facades\Auth;
* @method static \Illuminate\Database\Eloquent\Builder|User withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug) * @method static \Illuminate\Database\Eloquent\Builder|User withUniqueSlugConstraints(\Illuminate\Database\Eloquent\Model $model, string $attribute, array $config, string $slug)
* @mixin \Eloquent * @mixin \Eloquent
*/ */
final class User extends Authenticatable final class User extends Authenticatable implements HasMedia
{ {
use HasFactory, Notifiable, Sluggable; use HasFactory;
use InteractsWithMedia;
use Notifiable;
use Sluggable;
/** /**
* @inheritdoc * @inheritdoc
@ -115,4 +123,20 @@ final class User extends Authenticatable
}); });
return $goals; return $goals;
} }
/**
* Defines conversions for the User image.
*
* @throws \Spatie\Image\Exceptions\InvalidManipulation
*
* @see https://spatie.be/docs/laravel-medialibrary/v9/converting-images/defining-conversions
*/
public function registerMediaConversions(Media $media = null): void
{
$this->addMediaConversion('icon')
->width(300)
->height(300)
->sharpen(10)
->optimize();
}
} }

View File

@ -0,0 +1,29 @@
<?php
namespace App\View\Components\Inputs;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Model;
use Illuminate\View\Component;
class Image extends Component
{
public Model $model;
public ?string $previewName;
/**
* Image constructor.
*/
public function __construct(Model $model, string $previewName = 'preview') {
$this->model = $model;
$this->previewName = $previewName;
}
public function render(): View
{
return view('components.inputs.image')
->with('model', $this->model)
->with('previewName', $this->previewName);
}
}

2
public/css/app.css vendored

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
@if($model->hasMedia())
<div>
<div class="block font-medium text-sm text-gray-700 mb-1">Current image</div>
<a href="{{ $model->getFirstMedia()->getFullUrl() }}" target="_blank">
{{ $model->getFirstMedia()($previewName) }}
</a>
<fieldset class="flex space-x-2 mt-1 items-center">
<x-inputs.label for="remove_image" class="text-red-800" value="Remove this image" />
<x-inputs.input type="checkbox" name="remove_image" value="1" />
</fieldset>
</div>
@endif
<div>
@if($model->hasMedia())
<x-inputs.label for="image" value="Replace image" />
@else
<x-inputs.label for="image" value="Add image" />
@endif
<x-inputs.file name="image"
class="block mt-1 w-full"
accept="image/png, image/jpeg"/>
</div>

View File

@ -0,0 +1,17 @@
@php
$user_icon = null;
if ($user->hasMedia() && $user->getFirstMedia()->hasGeneratedConversion('icon')) {
$user_icon = $user->getFirstMediaUrl('default', 'icon');
}
@endphp
@empty($user_icon)
<svg class="h-10 w-10 fill-current text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
@else
<img {{ $attributes->merge([
'src' => $user_icon,
'class' => 'rounded-full h-10 w-10 flex items-center justify-center'
]) }} />
@endempty

View File

@ -21,13 +21,7 @@
<x-dropdown align="right" width="48"> <x-dropdown align="right" width="48">
<x-slot name="trigger"> <x-slot name="trigger">
<button class="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"> <button class="flex items-center text-sm font-medium text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out">
<div class="hidden sm:block">{{ Auth::user()->name }}</div> <x-user-icon :user="Auth::user()" />
<div class="ml-1">
<svg class="h-10 w-10 fill-current text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
</div>
</button> </button>
</x-slot> </x-slot>

View File

@ -79,29 +79,7 @@
<div class="flex flex-col space-y-4 mt-4"> <div class="flex flex-col space-y-4 mt-4">
<!-- Image --> <!-- Image -->
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0"> <div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
@if($recipe->hasMedia()) <x-inputs.image :model="$recipe" />
<div>
<div class="block font-medium text-sm text-gray-700 mb-1">Current image</div>
<a href="{{ $recipe->getFirstMedia()->getFullUrl() }}" target="_blank">
{{ $recipe->getFirstMedia()('preview') }}
</a>
<fieldset class="flex space-x-2 mt-1 items-center">
<x-inputs.label for="remove_image" class="text-red-800" value="Remove this image" />
<x-inputs.input type="checkbox" name="remove_image" value="1" />
</fieldset>
</div>
@endif
<div>
@if($recipe->hasMedia())
<x-inputs.label for="image" value="Replace image" />
@else
<x-inputs.label for="image" value="Add image" />
@endif
<x-inputs.file name="image"
class="block mt-1 w-full"
accept="image/png, image/jpeg"/>
</div>
</div> </div>
<!-- Description --> <!-- Description -->

View File

@ -4,7 +4,7 @@
<x-slot name="header"> <x-slot name="header">
<h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1> <h1 class="font-semibold text-xl text-gray-800 leading-tight">{{ $title }}</h1>
</x-slot> </x-slot>
<form method="POST" action="{{ ($user->exists ? route('users.update', $user) : route('users.store')) }}"> <form method="POST" enctype="multipart/form-data" action="{{ ($user->exists ? route('users.update', $user) : route('users.store')) }}">
@if ($user->exists)@method('put')@endif @if ($user->exists)@method('put')@endif
@csrf @csrf
<div class="flex flex-col space-y-4"> <div class="flex flex-col space-y-4">
@ -24,7 +24,7 @@
<!-- Name --> <!-- Name -->
<div class="flex-auto"> <div class="flex-auto">
<x-inputs.label for="name" value="Name"/> <x-inputs.label for="name" value="Display name"/>
<x-inputs.input name="name" <x-inputs.input name="name"
type="text" type="text"
@ -32,8 +32,11 @@
:value="old('name', $user->name)"/> :value="old('name', $user->name)"/>
</div> </div>
</div>
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
<!-- Password --> <!-- Password -->
<div> <div class="flex-auto">
<x-inputs.label for="password" value="Password"/> <x-inputs.label for="password" value="Password"/>
<x-inputs.input name="password" <x-inputs.input name="password"
@ -44,7 +47,7 @@
</div> </div>
<!-- Password confirm --> <!-- Password confirm -->
<div> <div class="flex-auto">
<x-inputs.label for="password_confirmation" value="Confirm Password"/> <x-inputs.label for="password_confirmation" value="Confirm Password"/>
<x-inputs.input name="password_confirmation" <x-inputs.input name="password_confirmation"
@ -53,17 +56,21 @@
:hasError="$errors->has('password')" :hasError="$errors->has('password')"
:required="!$user->exists"/> :required="!$user->exists"/>
</div> </div>
</div>
<!-- Admin --> <!-- Admin -->
<div> <div class="space-x-2">
<x-inputs.label for="admin" value="Site Admin"/> <x-inputs.label for="admin" value="Site Admin" class="inline-block"/>
<x-inputs.input name="admin" <x-inputs.input name="admin"
type="checkbox" type="checkbox"
value="1" value="1"
:checked="old('admin', $user->admin)" /> :checked="old('admin', $user->admin)" />
</div> </div>
<!-- Image -->
<div class="flex flex-col space-y-4 md:flex-row md:space-x-4 md:space-y-0">
<x-inputs.image :model="$user" preview_name="icon" />
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@
<tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-normal"> <tr class="bg-gray-200 text-gray-600 uppercase text-sm leading-normal">
<th class="py-3 px-6 text-left">Username</th> <th class="py-3 px-6 text-left">Username</th>
<th class="py-3 px-6 text-left">Name</th> <th class="py-3 px-6 text-left">Name</th>
<th class="py-3 px-6 text-left">Photo</th>
<th class="py-3 px-6 text-left">Admin</th> <th class="py-3 px-6 text-left">Admin</th>
<th class="py-3 px-6 text-left">Created</th> <th class="py-3 px-6 text-left">Created</th>
<th class="py-3 px-6 text-left">Updated</th> <th class="py-3 px-6 text-left">Updated</th>
@ -24,6 +25,9 @@
<tr class="border-b border-gray-200"> <tr class="border-b border-gray-200">
<td class="py-3 px-6">{{ $user->username }}</td> <td class="py-3 px-6">{{ $user->username }}</td>
<td class="py-3 px-6">{{ $user->name }}</td> <td class="py-3 px-6">{{ $user->name }}</td>
<td class="py-3 px-6">
<x-user-icon :user="$user" />
</td>
<td class="py-3 px-6">@if($user->admin) Yes @else No @endif</td> <td class="py-3 px-6">@if($user->admin) Yes @else No @endif</td>
<td class="py-3 px-6">{{ $user->created_at }}</td> <td class="py-3 px-6">{{ $user->created_at }}</td>
<td class="py-3 px-6">{{ $user->updated_at }}</td> <td class="py-3 px-6">{{ $user->updated_at }}</td>