mirror of https://github.com/kcal-app/kcal.git
Add User photo media support
This commit is contained in:
parent
f6fa2cf79f
commit
03f7319157
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'];
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue