Merge branch 'master' into pr/21

This commit is contained in:
Jonathan Reinink 2019-11-27 16:50:57 -05:00
commit e378e1c63a
30 changed files with 3433 additions and 1812 deletions

View file

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use League\Glide\Server;
class ImagesController extends Controller
{
public function show(Server $glide)
{
return $glide->fromRequest()->response();
}
}

View file

@ -25,6 +25,7 @@ class UsersController extends Controller
'name' => $user->name, 'name' => $user->name,
'email' => $user->email, 'email' => $user->email,
'owner' => $user->owner, 'owner' => $user->owner,
'photo' => $user->photoUrl(['w' => 40, 'h' => 40, 'fit' => 'crop']),
'deleted_at' => $user->deleted_at, 'deleted_at' => $user->deleted_at,
]; ];
}), }),
@ -38,15 +39,23 @@ class UsersController extends Controller
public function store() public function store()
{ {
Auth::user()->account->users()->create( Request::validate([
Request::validate([ 'first_name' => ['required', 'max:50'],
'first_name' => ['required', 'max:50'], 'last_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'], 'email' => ['required', 'max:50', 'email', Rule::unique('users')],
'email' => ['required', 'max:50', 'email', Rule::unique('users')], 'password' => ['nullable'],
'password' => ['nullable'], 'owner' => ['required', 'boolean'],
'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.'); return Redirect::route('users')->with('success', 'User created.');
} }
@ -60,6 +69,7 @@ class UsersController extends Controller
'last_name' => $user->last_name, 'last_name' => $user->last_name,
'email' => $user->email, 'email' => $user->email,
'owner' => $user->owner, 'owner' => $user->owner,
'photo' => $user->photoUrl(['w' => 60, 'h' => 60, 'fit' => 'crop']),
'deleted_at' => $user->deleted_at, 'deleted_at' => $user->deleted_at,
], ],
]); ]);
@ -73,10 +83,15 @@ class UsersController extends Controller
'email' => ['required', 'max:50', 'email', Rule::unique('users')->ignore($user->id)], 'email' => ['required', 'max:50', 'email', Rule::unique('users')->ignore($user->id)],
'password' => ['nullable'], 'password' => ['nullable'],
'owner' => ['required', 'boolean'], 'owner' => ['required', 'boolean'],
'photo' => ['nullable', 'image'],
]); ]);
$user->update(Request::only('first_name', 'last_name', 'email', 'owner')); $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')) { if (Request::get('password')) {
$user->update(['password' => Request::get('password')]); $user->update(['password' => Request::get('password')]);
} }

View file

@ -28,7 +28,6 @@ class Kernel extends HttpKernel
*/ */
protected $middlewareGroups = [ protected $middlewareGroups = [
'web' => [ 'web' => [
\Inertia\Middleware::class,
\App\Http\Middleware\EncryptCookies::class, \App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class, \Illuminate\Session\Middleware\StartSession::class,

View file

@ -18,7 +18,7 @@ class RedirectIfAuthenticated
public function handle($request, Closure $next, $guard = null) public function handle($request, Closure $next, $guard = null)
{ {
if (Auth::guard($guard)->check()) { if (Auth::guard($guard)->check()) {
return redirect('/home'); return redirect('/');
} }
return $next($request); return $next($request);

View file

@ -3,14 +3,15 @@
namespace App\Providers; namespace App\Providers;
use Inertia\Inertia; use Inertia\Inertia;
use League\Glide\Server;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Pagination\UrlWindow; use Illuminate\Pagination\UrlWindow;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
@ -22,17 +23,21 @@ class AppServiceProvider extends ServiceProvider
} }
public function register() public function register()
{
$this->registerInertia();
$this->registerGlide();
$this->registerLengthAwarePaginator();
}
public function registerInertia()
{ {
Inertia::version(function () { Inertia::version(function () {
return md5_file(public_path('mix-manifest.json')); return md5_file(public_path('mix-manifest.json'));
}); });
Inertia::share(function () { Inertia::share([
return [ 'auth' => function () {
'app' => [ return [
'name' => Config::get('app.name'),
],
'auth' => [
'user' => Auth::user() ? [ 'user' => Auth::user() ? [
'id' => Auth::user()->id, 'id' => Auth::user()->id,
'first_name' => Auth::user()->first_name, 'first_name' => Auth::user()->first_name,
@ -44,15 +49,31 @@ class AppServiceProvider extends ServiceProvider
'name' => Auth::user()->account->name, 'name' => Auth::user()->account->name,
], ],
] : null, ] : null,
], ];
'flash' => [ },
'flash' => function () {
return [
'success' => Session::get('success'), 'success' => Session::get('success'),
], ];
'errors' => Session::get('errors') ? Session::get('errors')->getBag('default')->getMessages() : (object) [], },
]; 'errors' => function () {
}); return Session::get('errors')
? Session::get('errors')->getBag('default')->getMessages()
: (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' => 'img',
]);
});
} }
protected function registerLengthAwarePaginator() protected function registerLengthAwarePaginator()

View file

@ -2,6 +2,9 @@
namespace App; namespace App;
use League\Glide\Server;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Auth\Authenticatable; use Illuminate\Auth\Authenticatable;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -32,6 +35,13 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac
$this->attributes['password'] = Hash::make($password); $this->attributes['password'] = Hash::make($password);
} }
public function photoUrl(array $attributes)
{
if ($this->photo_path) {
return URL::to(App::make(Server::class)->fromPath($this->photo_path, $attributes));
}
}
public function scopeOrderByName($query) public function scopeOrderByName($query)
{ {
$query->orderBy('last_name')->orderBy('first_name'); $query->orderBy('last_name')->orderBy('first_name');

View file

@ -5,22 +5,23 @@
"license": "MIT", "license": "MIT",
"type": "project", "type": "project",
"require": { "require": {
"php": "^7.1.3", "php": "^7.2",
"fideloper/proxy": "^4.0", "fideloper/proxy": "^4.0",
"fzaninotto/faker": "^1.4", "inertiajs/inertia-laravel": "^0.1",
"inertiajs/inertia-laravel": "dev-master", "laravel/framework": "^6.0",
"laravel/framework": "5.8.*",
"laravel/tinker": "^1.0", "laravel/tinker": "^1.0",
"league/glide": "2.0.x-dev",
"reinink/remember-query-strings": "^0.1.0", "reinink/remember-query-strings": "^0.1.0",
"tightenco/ziggy": "^0.6.9" "tightenco/ziggy": "^0.8.0"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.2", "barryvdh/laravel-debugbar": "^3.2",
"beyondcode/laravel-dump-server": "^1.0", "beyondcode/laravel-dump-server": "^1.0",
"filp/whoops": "^2.0", "facade/ignition": "^1.4",
"fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0", "mockery/mockery": "^1.0",
"nunomaduro/collision": "^2.0", "nunomaduro/collision": "^3.0",
"phpunit/phpunit": "^7.0" "phpunit/phpunit": "^8.0"
}, },
"autoload": { "autoload": {
"classmap": [ "classmap": [

1971
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
<?php <?php
use Faker\Generator as Faker; use Faker\Generator as Faker;
use Illuminate\Support\Str;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -19,7 +20,7 @@ $factory->define(App\User::class, function (Faker $faker) {
'last_name' => $faker->lastName, 'last_name' => $faker->lastName,
'email' => $faker->unique()->safeEmail, 'email' => $faker->unique()->safeEmail,
'password' => 'secret', 'password' => 'secret',
'remember_token' => str_random(10), 'remember_token' => Str::random(10),
'owner' => false, 'owner' => false,
]; ];
}); });

View file

@ -16,6 +16,7 @@ class CreateUsersTable extends Migration
$table->string('email', 50)->unique(); $table->string('email', 50)->unique();
$table->string('password')->nullable(); $table->string('password')->nullable();
$table->boolean('owner')->default(false); $table->boolean('owner')->default(false);
$table->string('photo_path', 100)->nullable();
$table->rememberToken(); $table->rememberToken();
$table->timestamps(); $table->timestamps();
$table->softDeletes(); $table->softDeletes();

2866
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,13 +11,14 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.2.0", "@babel/plugin-syntax-dynamic-import": "^7.2.0",
"@inertiajs/inertia": "^0.1.0",
"@inertiajs/inertia-vue": "^0.1.0",
"autosize": "^4.0.2", "autosize": "^4.0.2",
"axios": "^0.18", "axios": "^0.18",
"cross-env": "^5.1", "cross-env": "^5.1",
"eslint": "^5.14.1", "eslint": "^5.14.1",
"eslint-plugin-vue": "^5.2.2", "eslint-plugin-vue": "^5.2.2",
"fuse.js": "^3.4.2", "fuse.js": "^3.4.2",
"inertia-vue": "inertiajs/inertia-vue",
"laravel-mix": "^4.0.7", "laravel-mix": "^4.0.7",
"lodash": "^4.17.5", "lodash": "^4.17.5",
"popper.js": "^1.12", "popper.js": "^1.12",
@ -28,5 +29,8 @@
"tailwindcss": "^1.0.1", "tailwindcss": "^1.0.1",
"vue": "^2.6.6", "vue": "^2.6.6",
"vue-template-compiler": "^2.6.6" "vue-template-compiler": "^2.6.6"
},
"dependencies": {
"vue-meta": "^2.2.2"
} }
} }

View file

@ -22,7 +22,7 @@ composer install
Install NPM dependencies: Install NPM dependencies:
```sh ```sh
npm install npm ci
``` ```
Build assets: Build assets:

View file

@ -28,6 +28,7 @@ import Logo from '@/Shared/Logo'
import TextInput from '@/Shared/TextInput' import TextInput from '@/Shared/TextInput'
export default { export default {
metaInfo: { title: 'Login' },
components: { components: {
LoadingButton, LoadingButton,
Logo, Logo,
@ -46,9 +47,6 @@ export default {
}, },
} }
}, },
mounted() {
document.title = `Login | ${this.$page.app.name}`
},
methods: { methods: {
submit() { submit() {
this.sending = true this.sending = true

View file

@ -1,5 +1,5 @@
<template> <template>
<layout title="Create Contact"> <div>
<h1 class="mb-8 font-bold text-3xl"> <h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('contacts')">Contacts</inertia-link> <inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('contacts')">Contacts</inertia-link>
<span class="text-indigo-400 font-medium">/</span> Create <span class="text-indigo-400 font-medium">/</span> Create
@ -30,7 +30,7 @@
</div> </div>
</form> </form>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -40,8 +40,9 @@ import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput' import TextInput from '@/Shared/TextInput'
export default { export default {
metaInfo: { title: 'Create Contact' },
layout: (h, page) => h(Layout, [page]),
components: { components: {
Layout,
LoadingButton, LoadingButton,
SelectInput, SelectInput,
TextInput, TextInput,

View file

@ -1,5 +1,5 @@
<template> <template>
<layout :title="`${form.first_name} ${form.last_name}`"> <div>
<h1 class="mb-8 font-bold text-3xl"> <h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('contacts')">Contacts</inertia-link> <inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('contacts')">Contacts</inertia-link>
<span class="text-indigo-400 font-medium">/</span> <span class="text-indigo-400 font-medium">/</span>
@ -35,7 +35,7 @@
</div> </div>
</form> </form>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -46,8 +46,13 @@ import TextInput from '@/Shared/TextInput'
import TrashedMessage from '@/Shared/TrashedMessage' import TrashedMessage from '@/Shared/TrashedMessage'
export default { export default {
metaInfo() {
return {
title: `${this.form.first_name} ${this.form.last_name}`,
}
},
layout: (h, page) => h(Layout, [page]),
components: { components: {
Layout,
LoadingButton, LoadingButton,
SelectInput, SelectInput,
TextInput, TextInput,

View file

@ -1,5 +1,5 @@
<template> <template>
<layout title="Contacts"> <div>
<h1 class="mb-8 font-bold text-3xl">Contacts</h1> <h1 class="mb-8 font-bold text-3xl">Contacts</h1>
<div class="mb-6 flex justify-between items-center"> <div class="mb-6 flex justify-between items-center">
<search-filter v-model="form.search" class="w-full max-w-md mr-4" @reset="reset"> <search-filter v-model="form.search" class="w-full max-w-md mr-4" @reset="reset">
@ -59,7 +59,7 @@
</table> </table>
</div> </div>
<pagination :links="contacts.links" /> <pagination :links="contacts.links" />
</layout> </div>
</template> </template>
<script> <script>
@ -70,9 +70,10 @@ import Pagination from '@/Shared/Pagination'
import SearchFilter from '@/Shared/SearchFilter' import SearchFilter from '@/Shared/SearchFilter'
export default { export default {
metaInfo: { title: 'Contacts' },
layout: (h, page) => h(Layout, [page]),
components: { components: {
Icon, Icon,
Layout,
Pagination, Pagination,
SearchFilter, SearchFilter,
}, },

View file

@ -1,20 +1,19 @@
<template> <template>
<layout title="Dashboard"> <div>
<h1 class="mb-8 font-bold text-3xl">Dashboard</h1> <h1 class="mb-8 font-bold text-3xl">Dashboard</h1>
<p class="mb-12 leading-normal">Hey there! Welcome to Ping CRM, a demo app designed to help illustrate how <a class="text-indigo-500 underline hover:text-orange-600" href="https://github.com/inertiajs">Inertia.js</a> works.</p> <p class="mb-12 leading-normal">Hey there! Welcome to Ping CRM, a demo app designed to help illustrate how <a class="text-indigo-500 underline hover:text-orange-600" href="https://github.com/inertiajs">Inertia.js</a> works.</p>
<div> <div>
<inertia-link class="btn-indigo-500" href="/500">500 error</inertia-link> <inertia-link class="btn-indigo-500" href="/500">500 error</inertia-link>
<inertia-link class="btn-indigo-500" href="/404">404 error</inertia-link> <inertia-link class="btn-indigo-500" href="/404">404 error</inertia-link>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
import Layout from '@/Shared/Layout' import Layout from '@/Shared/Layout'
export default { export default {
components: { metaInfo: { title: 'Dashboard' },
Layout, layout: Layout,
},
} }
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<layout title="Create Organization"> <div>
<h1 class="mb-8 font-bold text-3xl"> <h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('organizations')">Organizations</inertia-link> <inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('organizations')">Organizations</inertia-link>
<span class="text-indigo-400 font-medium">/</span> Create <span class="text-indigo-400 font-medium">/</span> Create
@ -25,7 +25,7 @@
</div> </div>
</form> </form>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -35,8 +35,9 @@ import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput' import TextInput from '@/Shared/TextInput'
export default { export default {
metaInfo: { title: 'Create Organization' },
layout: (h, page) => h(Layout, [page]),
components: { components: {
Layout,
LoadingButton, LoadingButton,
SelectInput, SelectInput,
TextInput, TextInput,

View file

@ -1,5 +1,5 @@
<template> <template>
<layout :title="form.name"> <div>
<h1 class="mb-8 font-bold text-3xl"> <h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('organizations')">Organizations</inertia-link> <inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('organizations')">Organizations</inertia-link>
<span class="text-indigo-400 font-medium">/</span> <span class="text-indigo-400 font-medium">/</span>
@ -66,7 +66,7 @@
</tr> </tr>
</table> </table>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -78,9 +78,12 @@ import TextInput from '@/Shared/TextInput'
import TrashedMessage from '@/Shared/TrashedMessage' import TrashedMessage from '@/Shared/TrashedMessage'
export default { export default {
metaInfo() {
return { title: this.form.name }
},
layout: (h, page) => h(Layout, [page]),
components: { components: {
Icon, Icon,
Layout,
LoadingButton, LoadingButton,
SelectInput, SelectInput,
TextInput, TextInput,

View file

@ -1,5 +1,5 @@
<template> <template>
<layout title="Organizations"> <div>
<h1 class="mb-8 font-bold text-3xl">Organizations</h1> <h1 class="mb-8 font-bold text-3xl">Organizations</h1>
<div class="mb-6 flex justify-between items-center"> <div class="mb-6 flex justify-between items-center">
<search-filter v-model="form.search" class="w-full max-w-md mr-4" @reset="reset"> <search-filter v-model="form.search" class="w-full max-w-md mr-4" @reset="reset">
@ -51,7 +51,7 @@
</table> </table>
</div> </div>
<pagination :links="organizations.links" /> <pagination :links="organizations.links" />
</layout> </div>
</template> </template>
<script> <script>
@ -62,9 +62,10 @@ import Pagination from '@/Shared/Pagination'
import SearchFilter from '@/Shared/SearchFilter' import SearchFilter from '@/Shared/SearchFilter'
export default { export default {
metaInfo: { title: 'Organizations' },
layout: (h, page) => h(Layout, [page]),
components: { components: {
Icon, Icon,
Layout,
Pagination, Pagination,
SearchFilter, SearchFilter,
}, },

View file

@ -1,15 +1,14 @@
<template> <template>
<layout title="Reports"> <div>
<h1 class="mb-8 font-bold text-3xl">Reports</h1> <h1 class="mb-8 font-bold text-3xl">Reports</h1>
</layout> </div>
</template> </template>
<script> <script>
import Layout from '@/Shared/Layout' import Layout from '@/Shared/Layout'
export default { export default {
components: { metaInfo: { title: 'Reports' },
Layout, layout: (h, page) => h(Layout, [page]),
},
} }
</script> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<layout title="Create User"> <div>
<h1 class="mb-8 font-bold text-3xl"> <h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('users')">Users</inertia-link> <inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('users')">Users</inertia-link>
<span class="text-indigo-400 font-medium">/</span> Create <span class="text-indigo-400 font-medium">/</span> Create
@ -15,13 +15,14 @@
<option :value="true">Yes</option> <option :value="true">Yes</option>
<option :value="false">No</option> <option :value="false">No</option>
</select-input> </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>
<div class="px-8 py-4 bg-gray-100 border-t border-gray-200 flex justify-end items-center"> <div class="px-8 py-4 bg-gray-100 border-t border-gray-200 flex justify-end items-center">
<loading-button :loading="sending" class="btn-indigo-500" type="submit">Create User</loading-button> <loading-button :loading="sending" class="btn-indigo-500" type="submit">Create User</loading-button>
</div> </div>
</form> </form>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -29,13 +30,16 @@ import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton' import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput' import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput' import TextInput from '@/Shared/TextInput'
import FileInput from '@/Shared/FileInput'
export default { export default {
metaInfo: { title: 'Create User' },
layout: (h, page) => h(Layout, [page]),
components: { components: {
Layout,
LoadingButton, LoadingButton,
SelectInput, SelectInput,
TextInput, TextInput,
FileInput,
}, },
remember: 'form', remember: 'form',
data() { data() {
@ -47,13 +51,23 @@ export default {
email: null, email: null,
password: null, password: null,
owner: false, owner: false,
photo: null,
}, },
} }
}, },
methods: { methods: {
submit() { submit() {
this.sending = true 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) .then(() => this.sending = false)
}, },
}, },

View file

@ -1,14 +1,17 @@
<template> <template>
<layout :title="`${form.first_name} ${form.last_name}`"> <div>
<h1 class="mb-8 font-bold text-3xl"> <div class="mb-8 flex justify-start max-w-lg">
<inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('users')">Users</inertia-link> <h1 class="font-bold text-3xl">
<span class="text-indigo-400 font-medium">/</span> <inertia-link class="text-indigo-400 hover:text-indigo-600" :href="route('users')">Users</inertia-link>
{{ form.first_name }} {{ form.last_name }} <span class="text-indigo-400 font-medium">/</span>
</h1> {{ form.first_name }} {{ form.last_name }}
</h1>
<img v-if="user.photo" 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"> <trashed-message v-if="user.deleted_at" class="mb-6" @restore="restore">
This user has been deleted. This user has been deleted.
</trashed-message> </trashed-message>
<div class="bg-white rounded shadow overflow-hidden max-w-3xl"> <div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit"> <form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap"> <div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.first_name" :errors="$page.errors.first_name" class="pr-6 pb-8 w-full lg:w-1/2" label="First name" /> <text-input v-model="form.first_name" :errors="$page.errors.first_name" class="pr-6 pb-8 w-full lg:w-1/2" label="First name" />
@ -19,14 +22,15 @@
<option :value="true">Yes</option> <option :value="true">Yes</option>
<option :value="false">No</option> <option :value="false">No</option>
</select-input> </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>
<div class="px-8 py-4 bg-gray-100 border-t border-gray-200 flex items-center"> <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-700 hover:underline" tabindex="-1" type="button" @click="destroy">Delete User</button> <button v-if="!user.deleted_at" class="text-red hover:underline" tabindex="-1" type="button" @click="destroy">Delete User</button>
<loading-button :loading="sending" class="btn-indigo-500 ml-auto" type="submit">Update User</loading-button> <loading-button :loading="sending" class="btn-indigo ml-auto" type="submit">Update User</loading-button>
</div> </div>
</form> </form>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -34,14 +38,21 @@ import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton' import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput' import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput' import TextInput from '@/Shared/TextInput'
import FileInput from '@/Shared/FileInput'
import TrashedMessage from '@/Shared/TrashedMessage' import TrashedMessage from '@/Shared/TrashedMessage'
export default { export default {
metaInfo() {
return {
title: `${this.form.first_name} ${this.form.last_name}`,
}
},
layout: (h, page) => h(Layout, [page]),
components: { components: {
Layout,
LoadingButton, LoadingButton,
SelectInput, SelectInput,
TextInput, TextInput,
FileInput,
TrashedMessage, TrashedMessage,
}, },
props: { props: {
@ -57,14 +68,31 @@ export default {
email: this.user.email, email: this.user.email,
password: this.user.password, password: this.user.password,
owner: this.user.owner, owner: this.user.owner,
photo: null,
}, },
} }
}, },
methods: { methods: {
submit() { submit() {
this.sending = true this.sending = true
this.$inertia.put(this.route('users.update', this.user.id), this.form)
.then(() => this.sending = false) 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
if (Object.keys(this.$page.errors).length === 0) {
this.form.photo = null
this.form.password = null
}
})
}, },
destroy() { destroy() {
if (confirm('Are you sure you want to delete this user?')) { if (confirm('Are you sure you want to delete this user?')) {

View file

@ -1,5 +1,5 @@
<template> <template>
<layout title="Users"> <div>
<h1 class="mb-8 font-bold text-3xl">Users</h1> <h1 class="mb-8 font-bold text-3xl">Users</h1>
<div class="mb-6 flex justify-between items-center"> <div class="mb-6 flex justify-between items-center">
<search-filter v-model="form.search" class="w-full max-w-md mr-4" @reset="reset"> <search-filter v-model="form.search" class="w-full max-w-md mr-4" @reset="reset">
@ -31,6 +31,7 @@
<tr v-for="user in users" :key="user.id" class="hover:bg-gray-100 focus-within:bg-gray-100"> <tr v-for="user in users" :key="user.id" class="hover:bg-gray-100 focus-within:bg-gray-100">
<td class="border-t"> <td class="border-t">
<inertia-link class="px-6 py-4 flex items-center focus:text-indigo-500" :href="route('users.edit', user.id)"> <inertia-link class="px-6 py-4 flex items-center focus:text-indigo-500" :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 }} {{ user.name }}
<icon v-if="user.deleted_at" name="trash" class="flex-shrink-0 w-3 h-3 fill-gray-400 ml-2" /> <icon v-if="user.deleted_at" name="trash" class="flex-shrink-0 w-3 h-3 fill-gray-400 ml-2" />
</inertia-link> </inertia-link>
@ -56,7 +57,7 @@
</tr> </tr>
</table> </table>
</div> </div>
</layout> </div>
</template> </template>
<script> <script>
@ -66,9 +67,10 @@ import Layout from '@/Shared/Layout'
import SearchFilter from '@/Shared/SearchFilter' import SearchFilter from '@/Shared/SearchFilter'
export default { export default {
metaInfo: { title: 'Users' },
layout: (h, page) => h(Layout, [page]),
components: { components: {
Icon, Icon,
Layout,
SearchFilter, SearchFilter,
}, },
props: { props: {

View file

@ -0,0 +1,56 @@
<template>
<div>
<label v-if="label" class="form-label">{{ label }}:</label>
<div class="form-input p-0" :class="{ error: errors.length }">
<input ref="file" type="file" :accept="accept" class="hidden" @change="change">
<div v-if="!value" class="p-2">
<button type="button" class="px-4 py-1 bg-grey-dark hover:bg-grey-darker rounded-sm text-xs font-medium text-white" @click="browse">
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 type="button" class="px-4 py-1 bg-grey-dark hover:bg-grey-darker rounded-sm text-xs font-medium text-white" @click="remove">
Remove
</button>
</div>
</div>
<div v-if="errors.length" class="form-error">{{ errors[0] }}</div>
</div>
</template>
<script>
export default {
props: {
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>

View file

@ -2,7 +2,7 @@
<div> <div>
<portal-target name="dropdown" slim /> <portal-target name="dropdown" slim />
<div class="flex flex-col"> <div class="flex flex-col">
<div class="min-h-screen flex flex-col" @click="hideDropdownMenus"> <div class="h-screen flex flex-col" @click="hideDropdownMenus">
<div class="md:flex"> <div class="md:flex">
<div class="bg-indigo-900 md:flex-shrink-0 md:w-56 px-6 py-4 flex items-center justify-between md:justify-center"> <div class="bg-indigo-900 md:flex-shrink-0 md:w-56 px-6 py-4 flex items-center justify-between md:justify-center">
<inertia-link class="mt-1" href="/"> <inertia-link class="mt-1" href="/">
@ -10,8 +10,8 @@
</inertia-link> </inertia-link>
<dropdown class="md:hidden" placement="bottom-end"> <dropdown class="md:hidden" placement="bottom-end">
<svg class="fill-white w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" /></svg> <svg class="fill-white w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z" /></svg>
<div slot="dropdown" class="mt-2 px-8 py-4 shadow-xl bg-indigo-800 rounded"> <div slot="dropdown" class="mt-2 px-8 py-4 shadow-lg bg-indigo-800 rounded">
<main-menu /> <main-menu :url="url()" />
</div> </div>
</dropdown> </dropdown>
</div> </div>
@ -33,11 +33,9 @@
</dropdown> </dropdown>
</div> </div>
</div> </div>
<div class="flex flex-grow"> <div class="flex flex-grow overflow-hidden">
<div class="bg-indigo-800 flex-shrink-0 w-56 p-12 hidden md:block"> <main-menu :url="url()" class="bg-indigo-800 flex-no-shrink w-56 p-12 hidden md:block overflow-y-auto" />
<main-menu /> <div class="w-full overflow-hidden px-4 py-8 md:p-12 overflow-y-auto" scroll-region>
</div>
<div class="w-full overflow-hidden px-4 py-8 md:p-12">
<flash-messages /> <flash-messages />
<slot /> <slot />
</div> </div>
@ -62,26 +60,15 @@ export default {
Logo, Logo,
MainMenu, MainMenu,
}, },
props: {
title: String,
},
data() { data() {
return { return {
showUserMenu: false, showUserMenu: false,
accounts: null, accounts: null,
} }
}, },
watch: {
title(title) {
this.updatePageTitle(title)
},
},
mounted() {
this.updatePageTitle(this.title)
},
methods: { methods: {
updatePageTitle(title) { url() {
document.title = title ? `${title} | ${this.$page.app.name}` : this.$page.app.name return location.pathname.substr(1)
}, },
hideDropdownMenus() { hideDropdownMenus() {
this.showUserMenu = false this.showUserMenu = false

View file

@ -34,13 +34,16 @@ export default {
components: { components: {
Icon, Icon,
}, },
props: {
url: String,
},
methods: { methods: {
isUrl(...urls) { isUrl(...urls) {
if (urls[0] === '') { if (urls[0] === '') {
return location.pathname.substr(1) === '' return this.url === ''
} }
return urls.filter(url => location.pathname.substr(1).startsWith(url)).length return urls.filter(url => this.url.startsWith(url)).length
}, },
}, },
} }

16
resources/js/app.js vendored
View file

@ -1,16 +1,22 @@
import Inertia from 'inertia-vue'
import PortalVue from 'portal-vue'
import Vue from 'vue' import Vue from 'vue'
import VueMeta from 'vue-meta'
import PortalVue from 'portal-vue'
import { InertiaApp } from '@inertiajs/inertia-vue'
Vue.config.productionTip = false Vue.config.productionTip = false
Vue.mixin({ methods: { route: (...args) => window.route(...args).url() } }) Vue.mixin({ methods: { route: window.route } })
Vue.use(Inertia) Vue.use(InertiaApp)
Vue.use(PortalVue) Vue.use(PortalVue)
Vue.use(VueMeta)
let app = document.getElementById('app') let app = document.getElementById('app')
new Vue({ new Vue({
render: h => h(Inertia, { metaInfo: {
title: 'Loading…',
titleTemplate: '%s | Ping CRM',
},
render: h => h(InertiaApp, {
props: { props: {
initialPage: JSON.parse(app.dataset.page), initialPage: JSON.parse(app.dataset.page),
resolveComponent: name => import(`@/Pages/${name}`).then(module => module.default), resolveComponent: name => import(`@/Pages/${name}`).then(module => module.default),

View file

@ -12,8 +12,8 @@
*/ */
// Auth // Auth
Route::get('login')->name('login')->uses('Auth\LoginController@showLoginForm'); Route::get('login')->name('login')->uses('Auth\LoginController@showLoginForm')->middleware('guest');
Route::post('login')->name('login.attempt')->uses('Auth\LoginController@login'); Route::post('login')->name('login.attempt')->uses('Auth\LoginController@login')->middleware('guest');
Route::post('logout')->name('logout')->uses('Auth\LoginController@logout'); Route::post('logout')->name('logout')->uses('Auth\LoginController@logout');
// Dashboard // Dashboard
@ -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::delete('users/{user}')->name('users.destroy')->uses('UsersController@destroy')->middleware('auth');
Route::put('users/{user}/restore')->name('users.restore')->uses('UsersController@restore')->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 // Organizations
Route::get('organizations')->name('organizations')->uses('OrganizationsController@index')->middleware('remember', 'auth'); Route::get('organizations')->name('organizations')->uses('OrganizationsController@index')->middleware('remember', 'auth');
Route::get('organizations/create')->name('organizations.create')->uses('OrganizationsController@create')->middleware('auth'); Route::get('organizations/create')->name('organizations.create')->uses('OrganizationsController@create')->middleware('auth');