Commit 8fbfa660 by Jonathan Reinink

Add user photos

1 parent e9fd2577
<?php
namespace App\Http\Controllers;
use League\Glide\Server;
class ImagesController extends Controller
{
public function show(Server $glide)
{
return $glide->fromRequest()->response();
}
}
......@@ -25,6 +25,7 @@ class UsersController extends Controller
'name' => $user->name,
'email' => $user->email,
'owner' => $user->owner,
'photo' => $user->photoUrl(['w' => 40, 'h' => 40, 'fit' => 'crop']),
'deleted_at' => $user->deleted_at,
];
}),
......@@ -38,15 +39,23 @@ class UsersController extends Controller
public function store()
{
Auth::user()->account->users()->create(
Request::validate([
'first_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'],
'email' => ['required', 'max:50', 'email', Rule::unique('users')],
'password' => ['nullable'],
'owner' => ['required', 'boolean'],
])
);
Request::validate([
'first_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'],
'email' => ['required', 'max:50', 'email', Rule::unique('users')],
'password' => ['nullable'],
'owner' => ['required', 'boolean'],
'photo' => ['nullable', 'image'],
]);
Auth::user()->account->users()->create([
'first_name' => Request::get('first_name'),
'last_name' => Request::get('last_name'),
'email' => Request::get('email'),
'password' => Request::get('password'),
'owner' => Request::get('owner'),
'photo_path' => Request::file('photo') ? Request::file('photo')->store('users') : null,
]);
return Redirect::route('users')->with('success', 'User created.');
}
......@@ -60,6 +69,7 @@ class UsersController extends Controller
'last_name' => $user->last_name,
'email' => $user->email,
'owner' => $user->owner,
'photo' => $user->photoUrl(['w' => 60, 'h' => 60, 'fit' => 'crop']),
'deleted_at' => $user->deleted_at,
],
]);
......@@ -73,10 +83,15 @@ class UsersController extends Controller
'email' => ['required', 'max:50', 'email', Rule::unique('users')->ignore($user->id)],
'password' => ['nullable'],
'owner' => ['required', 'boolean'],
'photo' => ['nullable', 'image'],
]);
$user->update(Request::only('first_name', 'last_name', 'email', 'owner'));
if (Request::file('photo')) {
$user->update(['photo_path' => Request::file('photo')->store('users')]);
}
if (Request::get('password')) {
$user->update(['password' => Request::get('password')]);
}
......
......@@ -3,14 +3,17 @@
namespace App\Providers;
use Inertia\Inertia;
use League\Glide\Server;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\URL;
use Illuminate\Pagination\UrlWindow;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator;
......@@ -23,6 +26,13 @@ class AppServiceProvider extends ServiceProvider
public function register()
{
$this->registerInertia();
$this->registerGlide();
$this->registerLengthAwarePaginator();
}
public function registerInertia()
{
Inertia::version(function () {
return md5_file(public_path('mix-manifest.json'));
});
......@@ -50,8 +60,18 @@ class AppServiceProvider extends ServiceProvider
: (object) [],
];
});
}
$this->registerLengthAwarePaginator();
protected function registerGlide()
{
$this->app->bind(Server::class, function ($app) {
return Server::create([
'source' => Storage::getDriver(),
'cache' => Storage::getDriver(),
'cache_folder' => '.glide-cache',
'base_url' => URL::to('img'),
]);
});
}
protected function registerLengthAwarePaginator()
......
......@@ -2,6 +2,8 @@
namespace App;
use League\Glide\Server;
use Illuminate\Support\Facades\App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Eloquent\SoftDeletes;
......@@ -32,6 +34,15 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
$this->attributes['password'] = Hash::make($password);
}
public function photoUrl(array $attributes)
{
if (!$this->photo_path) {
return;
}
return App::make(Server::class)->fromPath($this->photo_path, $attributes)->url();
}
public function scopeOrderByName($query)
{
$query->orderBy('last_name')->orderBy('first_name');
......
......@@ -11,6 +11,7 @@
"inertiajs/inertia-laravel": "dev-master",
"laravel/framework": "5.8.*",
"laravel/tinker": "^1.0",
"league/glide": "2.0.x-dev",
"reinink/remember-query-strings": "^0.1.0",
"tightenco/ziggy": "^0.6.9"
},
......
......@@ -16,6 +16,7 @@ class CreateUsersTable extends Migration
$table->string('email', 50)->unique();
$table->string('password')->nullable();
$table->boolean('owner')->default(false);
$table->string('photo_path', 100)->nullable();
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
......
......@@ -15,6 +15,7 @@
<option :value="true">Yes</option>
<option :value="false">No</option>
</select-input>
<file-input v-model="form.photo" :errors="$page.errors.photo" class="pr-6 pb-8 w-full lg:w-1/2" type="file" accept="image/*" label="Photo" />
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex justify-end items-center">
<loading-button :loading="sending" class="btn-indigo" type="submit">Create User</loading-button>
......@@ -29,6 +30,7 @@ import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
import FileInput from '@/Shared/FileInput'
export default {
components: {
......@@ -36,24 +38,35 @@ export default {
LoadingButton,
SelectInput,
TextInput,
FileInput,
},
remember: 'form',
data() {
return {
sending: false,
form: {
first_name: null,
last_name: null,
email: null,
password: null,
first_name: '',
last_name: '',
email: '',
password: '',
owner: false,
photo: '',
},
}
},
methods: {
submit() {
this.sending = true
this.$inertia.post(this.route('users.store'), this.form)
var data = new FormData()
data.append('first_name', this.form.first_name)
data.append('last_name', this.form.last_name)
data.append('email', this.form.email)
data.append('password', this.form.password)
data.append('owner', this.form.owner ? '1' : '0')
data.append('photo', this.form.photo)
this.$inertia.post(this.route('users.store'), data)
.then(() => this.sending = false)
},
},
......
<template>
<layout :title="`${form.first_name} ${form.last_name}`">
<h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-light hover:text-indigo-dark" :href="route('users')">Users</inertia-link>
<span class="text-indigo-light font-medium">/</span>
{{ form.first_name }} {{ form.last_name }}
</h1>
<div class="mb-8 flex justify-start max-w-lg">
<h1 class="font-bold text-3xl">
<inertia-link class="text-indigo-light hover:text-indigo-dark" :href="route('users')">Users</inertia-link>
<span class="text-indigo-light font-medium">/</span>
{{ form.first_name }} {{ form.last_name }}
</h1>
<img class="block w-8 h-8 rounded-full ml-4" :src="user.photo">
</div>
<trashed-message v-if="user.deleted_at" class="mb-6" @restore="restore">
This user has been deleted.
</trashed-message>
......@@ -19,6 +22,7 @@
<option :value="true">Yes</option>
<option :value="false">No</option>
</select-input>
<file-input v-model="form.photo" :errors="$page.errors.photo" class="pr-6 pb-8 w-full lg:w-1/2" type="file" accept="image/*" label="Photo" />
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex items-center">
<button v-if="!user.deleted_at" class="text-red hover:underline" tabindex="-1" type="button" @click="destroy">Delete User</button>
......@@ -34,6 +38,7 @@ import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
import FileInput from '@/Shared/FileInput'
import TrashedMessage from '@/Shared/TrashedMessage'
export default {
......@@ -42,6 +47,7 @@ export default {
LoadingButton,
SelectInput,
TextInput,
FileInput,
TrashedMessage,
},
props: {
......@@ -57,13 +63,24 @@ export default {
email: this.user.email,
password: this.user.password,
owner: this.user.owner,
photo: '',
},
}
},
methods: {
submit() {
this.sending = true
this.$inertia.put(this.route('users.update', this.user.id), this.form)
var data = new FormData()
data.append('first_name', this.form.first_name)
data.append('last_name', this.form.last_name)
data.append('email', this.form.email)
data.append('password', this.form.password)
data.append('owner', this.form.owner ? '1' : '0')
data.append('photo', this.form.photo)
data.append('_method', 'put')
this.$inertia.post(this.route('users.update', this.user.id), data)
.then(() => this.sending = false)
},
destroy() {
......
......@@ -31,6 +31,7 @@
<tr v-for="user in users" :key="user.id" class="hover:bg-grey-lightest focus-within:bg-grey-lightest">
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center focus:text-indigo" :href="route('users.edit', user.id)">
<img v-if="user.photo" class="block w-5 h-5 rounded-full mr-2 -my-2" :src="user.photo">
{{ user.name }}
<icon v-if="user.deleted_at" name="trash" class="flex-no-shrink w-3 h-3 fill-grey ml-2" />
</inertia-link>
......
<template>
<div>
<label v-if="label" class="form-label">{{ label }}:</label>
<div class="form-input p-0" :class="{ error: errors.length }">
<input class="hidden" ref="file" type="file" @change="change" :accept="accept">
<div v-if="!value" class="p-2">
<button @click="browse" type="button" class="px-4 py-1 bg-grey-dark hover:bg-grey-darker rounded-sm text-xs font-medium text-white">
Browse
</button>
</div>
<div v-else class="flex items-center justify-between p-2">
<div class="flex-1 pr-1">{{ value.name }} <span class="text-grey-dark text-xs">({{ filesize(value.size) }})</span></div>
<button @click="remove" type="button" class="px-4 py-1 bg-grey-dark hover:bg-grey-darker rounded-sm text-xs font-medium text-white">
Remove
</button>
</div>
</div>
<div v-if="errors.length" class="form-error">{{ errors[0] }}</div>
</div>
</template>
<script>
export default {
props: {
id: {
type: String,
default() {
return `text-input-${this._uid}`
},
},
value: File,
label: String,
accept: String,
errors: {
type: Array,
default: () => [],
},
},
watch: {
value(value) {
if (!value) {
this.$refs.file.value = ''
}
}
},
methods: {
filesize(size) {
var i = Math.floor(Math.log(size) / Math.log(1024))
return (size / Math.pow(1024, i) ).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i]
},
browse() {
this.$refs.file.click()
},
change(e) {
this.$emit('input', e.target.files[0])
},
remove() {
this.$emit('input', null)
},
}
}
</script>
\ No newline at end of file
......@@ -28,6 +28,9 @@ Route::put('users/{user}')->name('users.update')->uses('UsersController@update')
Route::delete('users/{user}')->name('users.destroy')->uses('UsersController@destroy')->middleware('auth');
Route::put('users/{user}/restore')->name('users.restore')->uses('UsersController@restore')->middleware('auth');
// Images
Route::get('/img/{path}', 'ImagesController@show')->where('path', '.*');
// Organizations
Route::get('organizations')->name('organizations')->uses('OrganizationsController@index')->middleware('remember', 'auth');
Route::get('organizations/create')->name('organizations.create')->uses('OrganizationsController@create')->middleware('auth');
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!