Initial commit

This commit is contained in:
Jonathan Reinink 2019-03-18 07:53:00 -04:00
parent d0531481eb
commit 14192d0e46
76 changed files with 14241 additions and 465 deletions

3
.babelrc Normal file
View File

@ -0,0 +1,3 @@
{
"plugins": ["@babel/plugin-syntax-dynamic-import"]
}

15
.eslintrc.js vendored Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:vue/recommended',
],
rules: {
"indent": ['error', 2],
'quotes': ['warn', 'single'],
'semi': ['warn', 'never'],
'comma-dangle': ['warn', 'always-multiline'],
'vue/max-attributes-per-line': false,
'vue/require-default-prop': false,
'vue/singleline-html-element-content-newline': false,
}
}

5
.gitignore vendored
View File

@ -1,9 +1,14 @@
/node_modules /node_modules
/public/css
/public/hot /public/hot
/public/js
/public/mix-manifest.json
/public/storage /public/storage
/storage/*.key /storage/*.key
/vendor /vendor
.DS_Store
.env .env
.php_cs.dist
.phpunit.result.cache .phpunit.result.cache
Homestead.json Homestead.json
Homestead.yaml Homestead.yaml

23
app/Account.php Normal file
View File

@ -0,0 +1,23 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\SoftDeletes;
class Account extends Model
{
public function users()
{
return $this->hasMany(User::class);
}
public function organizations()
{
return $this->hasMany(Organization::class);
}
public function contacts()
{
return $this->hasMany(Contact::class);
}
}

47
app/Contact.php Normal file
View File

@ -0,0 +1,47 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\SoftDeletes;
class Contact extends Model
{
use SoftDeletes;
public function organization()
{
return $this->belongsTo(Organization::class);
}
public function getNameAttribute()
{
return $this->first_name.' '.$this->last_name;
}
public function scopeOrderByName($query)
{
$query->orderBy('last_name')->orderBy('first_name');
}
public function scopeFilter($query, array $filters)
{
$query->when($filters['search'] ?? null, function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->where('first_name', 'ilike', '%'.$search.'%')
->orWhere('last_name', 'ilike', '%'.$search.'%')
->orWhere('email', 'ilike', '%'.$search.'%')
->orWhereHas('organization', function ($query) use ($search) {
$query->where('name', 'ilike', '%'.$search.'%');
});
});
// })->when($filters['role'] ?? null, function ($query, $role) {
// $query->whereRole($role);
})->when($filters['trashed'] ?? null, function ($query, $trashed) {
if ($trashed === 'with') {
$query->withTrashed();
} elseif ($trashed === 'only') {
$query->onlyTrashed();
}
});
}
}

View File

@ -2,7 +2,13 @@
namespace App\Http\Controllers\Auth; namespace App\Http\Controllers\Auth;
use Inertia\Inertia;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\URL;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Response;
use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller class LoginController extends Controller
@ -25,15 +31,22 @@ class LoginController extends Controller
* *
* @var string * @var string
*/ */
protected $redirectTo = '/home'; protected $redirectTo = '/';
/** public function showLoginForm()
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{ {
$this->middleware('guest')->except('logout'); return Inertia::render('Auth/Login', [
'intendedUrl' => Session::pull('url.intended', URL::route('dashboard')),
]);
}
protected function authenticated(Request $request, $user)
{
return Response::json(['success' => true]);
}
protected function loggedOut(Request $request)
{
return Redirect::route('login');
} }
} }

View File

@ -0,0 +1,120 @@
<?php
namespace App\Http\Controllers;
use App\Contact;
use Inertia\Inertia;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
class ContactsController extends Controller
{
public function index()
{
return Inertia::render('Contacts/Index', [
'filters' => Request::all('search', 'trashed'),
'contacts' => Auth::user()->account->contacts()
->orderByName()
->filter(Request::only('search', 'trashed'))
->paginate()
->transform(function ($contact) {
return [
'id' => $contact->id,
'name' => $contact->name,
'phone' => $contact->phone,
'city' => $contact->city,
'deleted_at' => $contact->deleted_at,
'organization' => $contact->organization ? $contact->organization->only('name') : null,
];
}),
]);
}
public function create()
{
return Inertia::render('Contacts/Create', [
'organizations' => Auth::user()->account
->organizations()
->orderBy('name')
->get()
->map
->only('id', 'name'),
]);
}
public function store()
{
return Auth::user()->account->contacts()->create(
Request::validate([
'first_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'],
'organization_id' => ['nullable', Rule::exists('organizations', 'id')->where(function ($query) {
$query->where('account_id', Auth::user()->account_id);
})],
'email' => ['nullable', 'max:50', 'email'],
'phone' => ['nullable', 'max:50'],
'address' => ['nullable', 'max:150'],
'city' => ['nullable', 'max:50'],
'region' => ['nullable', 'max:50'],
'country' => ['nullable', 'max:2'],
'postal_code' => ['nullable', 'max:25'],
])
)->only('id');
}
public function edit(Contact $contact)
{
return Inertia::render('Contacts/Edit', [
'contact' => [
'id' => $contact->id,
'first_name' => $contact->first_name,
'last_name' => $contact->last_name,
'organization_id' => $contact->organization_id,
'email' => $contact->email,
'phone' => $contact->phone,
'address' => $contact->address,
'city' => $contact->city,
'region' => $contact->region,
'country' => $contact->country,
'postal_code' => $contact->postal_code,
'deleted_at' => $contact->deleted_at,
],
'organizations' => Auth::user()->account->organizations()
->orderBy('name')
->get()
->map
->only('id', 'name'),
]);
}
public function update(Contact $contact)
{
$contact->update(
Request::validate([
'first_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'],
'organization_id' => ['nullable', Rule::exists('organizations', 'id')->where(function ($query) {
$query->where('account_id', Auth::user()->account_id);
})],
'email' => ['nullable', 'max:50', 'email'],
'phone' => ['nullable', 'max:50'],
'address' => ['nullable', 'max:150'],
'city' => ['nullable', 'max:50'],
'region' => ['nullable', 'max:50'],
'country' => ['nullable', 'max:2'],
'postal_code' => ['nullable', 'max:25'],
])
);
}
public function destroy(Contact $contact)
{
$contact->delete();
}
public function restore(Contact $contact)
{
$contact->restore();
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class DashboardController extends Controller
{
public function index()
{
return Inertia::render('Dashboard/Index');
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
use App\Organization;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
class OrganizationsController extends Controller
{
public function index()
{
return Inertia::render('Organizations/Index', [
'filters' => Request::all('search', 'role', 'trashed'),
'organizations' => Auth::user()->account->organizations()
->orderBy('name')
->filter(Request::only('search', 'role', 'trashed'))
->paginate()
->only('id', 'name', 'phone', 'city', 'deleted_at'),
]);
}
public function create()
{
return Inertia::render('Organizations/Create');
}
public function store()
{
return Auth::user()->account->organizations()->create(
Request::validate([
'name' => ['required', 'max:100'],
'email' => ['nullable', 'max:50', 'email'],
'phone' => ['nullable', 'max:50'],
'address' => ['nullable', 'max:150'],
'city' => ['nullable', 'max:50'],
'region' => ['nullable', 'max:50'],
'country' => ['nullable', 'max:2'],
'postal_code' => ['nullable', 'max:25'],
])
)->only('id');
}
public function edit(Organization $organization)
{
return Inertia::render('Organizations/Edit', [
'organization' => [
'id' => $organization->id,
'name' => $organization->name,
'email' => $organization->email,
'phone' => $organization->phone,
'address' => $organization->address,
'city' => $organization->city,
'region' => $organization->region,
'country' => $organization->country,
'postal_code' => $organization->postal_code,
'deleted_at' => $organization->deleted_at,
'contacts' => $organization->contacts()->orderByName()->get()->map->only('id', 'name', 'city', 'phone'),
],
]);
}
public function update(Organization $organization)
{
$organization->update(
Request::validate([
'name' => ['required', 'max:100'],
'email' => ['nullable', 'max:50', 'email'],
'phone' => ['nullable', 'max:50'],
'address' => ['nullable', 'max:150'],
'city' => ['nullable', 'max:50'],
'region' => ['nullable', 'max:50'],
'country' => ['nullable', 'max:2'],
'postal_code' => ['nullable', 'max:25'],
])
);
}
public function destroy(Organization $organization)
{
$organization->delete();
}
public function restore(Organization $organization)
{
$organization->restore();
}
}

View File

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class ReportsController extends Controller
{
public function index()
{
return Inertia::render('Reports/Index');
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers;
use App\User;
use Inertia\Inertia;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Request;
class UsersController extends Controller
{
public function index()
{
return Inertia::render('Users/Index', [
'filters' => Request::all('search', 'role', 'trashed'),
'users' => Auth::user()->account->users()
->orderByName()
->filter(Request::only('search', 'role', 'trashed'))
->get()
->transform(function ($user) {
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'owner' => $user->owner,
'deleted_at' => $user->deleted_at,
];
}),
]);
}
public function create()
{
return Inertia::render('Users/Create');
}
public function store()
{
return 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'],
])
)->only('id');
}
public function edit(User $user)
{
return Inertia::render('Users/Edit', [
'user' => [
'id' => $user->id,
'first_name' => $user->first_name,
'last_name' => $user->last_name,
'email' => $user->email,
'owner' => $user->owner,
'deleted_at' => $user->deleted_at,
],
]);
}
public function update(User $user)
{
Request::validate([
'first_name' => ['required', 'max:50'],
'last_name' => ['required', 'max:50'],
'email' => ['required', 'max:50', 'email', Rule::unique('users')->ignore($user->id)],
'password' => ['nullable'],
'owner' => ['required', 'boolean'],
]);
$user->update(Request::only('first_name', 'last_name', 'email', 'owner'));
if (Request::get('password')) {
$user->update(['password' => Request::get('password')]);
}
}
public function destroy(User $user)
{
$user->delete();
}
public function restore(User $user)
{
$user->restore();
}
}

View File

@ -59,6 +59,7 @@ class Kernel extends HttpKernel
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'remember' => \App\Http\Middleware\RememberQueryStrings::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
]; ];

View File

@ -0,0 +1,56 @@
<?php
namespace App\Http\Middleware;
use Closure;
class RememberQueryStrings
{
public function handle($request, Closure $next)
{
if ($request->wantsJson()) {
return $next($request);
}
if (empty($request->all())) {
return $this->remembered($next, $request);
}
if ($request->get('remember') === 'no') {
return $next($request);
}
if ($request->get('remember') === 'forget') {
return $this->forget($next, $request);
}
return $this->remember($next, $request);
}
protected function remembered($next, $request)
{
$remembered = array_filter($request->session()->get('remember_query_strings.'.$request->route()->getName()) ?? []);
if ($remembered) {
$request->session()->reflash();
return redirect(url($request->path()).'?'.http_build_query($remembered));
}
return $next($request);
}
protected function remember($next, $request)
{
$request->session()->put('remember_query_strings.'.$request->route()->getName(), array_filter($request->all()));
return $next($request);
}
protected function forget($next, $request)
{
$request->session()->remove('remember_query_strings.'.$request->route()->getName());
return redirect(url($request->path()));
}
}

21
app/Model.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace App;
use Illuminate\Support\Facades\App;
use Illuminate\Database\Eloquent\Model as Eloquent;
abstract class Model extends Eloquent
{
protected $guarded = [];
public function getPerPage()
{
return 10;
}
public function resolveRouteBinding($value)
{
return $this->where('id', $value)->withTrashed()->first() ?? App::abort(404);
}
}

28
app/Organization.php Normal file
View File

@ -0,0 +1,28 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\SoftDeletes;
class Organization extends Model
{
use SoftDeletes;
public function contacts()
{
return $this->hasMany(Contact::class);
}
public function scopeFilter($query, array $filters)
{
$query->when($filters['search'] ?? null, function ($query, $search) {
$query->where('name', 'ilike', '%'.$search.'%');
})->when($filters['trashed'] ?? null, function ($query, $trashed) {
if ($trashed === 'with') {
$query->withTrashed();
} elseif ($trashed === 'only') {
$query->onlyTrashed();
}
});
}
}

View File

@ -2,27 +2,129 @@
namespace App\Providers; namespace App\Providers;
use Debugbar;
use Inertia\Inertia;
use OpenPsa\Ranger\Ranger;
use Carbon\CarbonImmutable;
use Illuminate\Support\Collection;
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\ServiceProvider; use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator;
class AppServiceProvider extends ServiceProvider class AppServiceProvider extends ServiceProvider
{ {
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot() public function boot()
{ {
// Debugbar::disable();
Date::use(CarbonImmutable::class);
}
public function register()
{
Inertia::share('app.name', Config::get('app.name'));
Inertia::share('auth.user', function () {
if (Auth::user()) {
return [
'id' => Auth::user()->id,
'first_name' => Auth::user()->first_name,
'last_name' => Auth::user()->last_name,
'email' => Auth::user()->email,
'role' => Auth::user()->role,
'account' => [
'id' => Auth::user()->account->id,
'name' => Auth::user()->account->name,
],
];
}
});
$this->registerLengthAwarePaginator();
$this->registerCarbonMarcos();
}
protected function registerLengthAwarePaginator()
{
$this->app->bind(LengthAwarePaginator::class, function ($app, $values) {
return new class(...array_values($values)) extends LengthAwarePaginator {
public function only(...$attributes)
{
return $this->transform(function ($item) use ($attributes) {
return $item->only($attributes);
});
}
public function transform($callback)
{
$this->items->transform($callback);
return $this;
}
public function toArray()
{
return [
'data' => $this->items->toArray(),
'links' => $this->links(),
];
}
public function links($view = null, $data = [])
{
$this->appends(Request::all());
$window = UrlWindow::make($this);
$elements = array_filter([
$window['first'],
is_array($window['slider']) ? '...' : null,
$window['slider'],
is_array($window['last']) ? '...' : null,
$window['last'],
]);
return Collection::make($elements)->flatMap(function ($item) {
if (is_array($item)) {
return Collection::make($item)->map(function ($url, $page) {
return [
'url' => $url,
'label' => $page,
'active' => $this->currentPage() === $page,
];
});
} else {
return [
[
'url' => null,
'label' => '...',
'active' => false,
],
];
}
})->prepend([
'url' => $this->previousPageUrl(),
'label' => 'Previous',
'active' => false,
])->push([
'url' => $this->nextPageUrl(),
'label' => 'Next',
'active' => false,
]);
}
};
});
}
protected function registerCarbonMarcos()
{
CarbonImmutable::macro('range', function ($to) {
return (new Ranger('en'))->format(
$this->toDateString(),
$to->toDateString()
);
});
} }
} }

View File

@ -2,38 +2,61 @@
namespace App; namespace App;
use Illuminate\Notifications\Notifiable; use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Support\Facades\Hash;
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
class User extends Authenticatable class User extends Model implements AuthenticatableContract, AuthorizableContract
{ {
use Notifiable; use SoftDeletes, Authenticatable, Authorizable;
/** public function account()
* The attributes that are mass assignable. {
* return $this->belongsTo(Account::class);
* @var array }
*/
protected $fillable = [
'name', 'email', 'password',
];
/** public function getNameAttribute()
* The attributes that should be hidden for arrays. {
* return $this->first_name.' '.$this->last_name;
* @var array }
*/
protected $hidden = [
'password', 'remember_token',
];
/** public function setPasswordAttribute($password)
* The attributes that should be cast to native types. {
* $this->attributes['password'] = Hash::make($password);
* @var array }
*/
protected $casts = [ public function scopeOrderByName($query)
'email_verified_at' => 'datetime', {
]; $query->orderBy('last_name')->orderBy('first_name');
}
public function scopeWhereRole($query, $role)
{
switch ($role) {
case 'user': return $query->where('owner', false);
case 'owner': return $query->where('owner', true);
}
}
public function scopeFilter($query, array $filters)
{
$query->when($filters['search'] ?? null, function ($query, $search) {
$query->where(function ($query) use ($search) {
$query->where('first_name', 'ilike', '%'.$search.'%')
->orWhere('last_name', 'ilike', '%'.$search.'%')
->orWhere('email', 'ilike', '%'.$search.'%');
});
})->when($filters['role'] ?? null, function ($query, $role) {
$query->whereRole($role);
})->when($filters['trashed'] ?? null, function ($query, $trashed) {
if ($trashed === 'with') {
$query->withTrashed();
} elseif ($trashed === 'only') {
$query->onlyTrashed();
}
});
}
} }

View File

@ -1,62 +1,81 @@
{ {
"name": "laravel/laravel", "name": "laravel/laravel",
"type": "project",
"description": "The Laravel Framework.", "description": "The Laravel Framework.",
"keywords": [ "keywords": ["framework", "laravel"],
"framework",
"laravel"
],
"license": "MIT", "license": "MIT",
"type": "project",
"repositories": [
{
"type": "path",
"url": "../inertia-laravel"
}
],
"require": { "require": {
"php": "^7.1.3", "php": "~7.2.0",
"ext-redis": "*",
"fideloper/proxy": "^4.0", "fideloper/proxy": "^4.0",
"fzaninotto/faker": "^1.4",
"laravel/framework": "5.8.*", "laravel/framework": "5.8.*",
"laravel/tinker": "^1.0" "laravel/tinker": "^1.0",
"openpsa/ranger": "^0.4.0",
"reinink/advanced-eloquent": "^0.2.0",
"reinink/inertia-laravel": "@dev",
"tightenco/ziggy": "^0.6.9"
}, },
"require-dev": { "require-dev": {
"barryvdh/laravel-debugbar": "^3.2",
"beyondcode/laravel-dump-server": "^1.0", "beyondcode/laravel-dump-server": "^1.0",
"filp/whoops": "^2.0", "filp/whoops": "^2.0",
"fzaninotto/faker": "^1.4",
"mockery/mockery": "^1.0", "mockery/mockery": "^1.0",
"nunomaduro/collision": "^2.0", "nunomaduro/collision": "^2.0",
"phpunit/phpunit": "^7.5" "phpunit/phpunit": "^7.0"
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true
},
"extra": {
"laravel": {
"dont-discover": []
}
}, },
"autoload": { "autoload": {
"psr-4": {
"App\\": "app/"
},
"classmap": [ "classmap": [
"database/seeds", "database/seeds",
"database/factories" "database/factories"
] ],
"psr-4": {
"App\\": "app/"
}
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Tests\\": "tests/" "Tests\\": "tests/"
} }
}, },
"minimum-stability": "dev", "extra": {
"prefer-stable": true, "laravel": {
"dont-discover": [
]
}
},
"scripts": { "scripts": {
"post-autoload-dump": [ "compile": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "npm run prod",
"@php artisan package:discover --ansi" "@php artisan vendor:publish --provider=\"Laravel\\Horizon\\HorizonServiceProvider\"",
"@php artisan migrate --force"
],
"reseed": [
"@php artisan migrate:fresh",
"@php artisan db:seed"
], ],
"post-root-package-install": [ "post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
], ],
"post-create-project-cmd": [ "post-create-project-cmd": [
"@php artisan key:generate --ansi" "@php artisan key:generate"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
] ]
} },
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true
} }

561
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "4731429c7f25ab92eaa813447a972209", "content-hash": "1a9f92a6ff3cce7a387436a576d6b8a9",
"packages": [ "packages": [
{ {
"name": "dnoegel/php-xdg-base-dir", "name": "dnoegel/php-xdg-base-dir",
@ -366,6 +366,56 @@
], ],
"time": "2019-01-10T14:06:47+00:00" "time": "2019-01-10T14:06:47+00:00"
}, },
{
"name": "fzaninotto/faker",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/fzaninotto/Faker.git",
"reference": "f72816b43e74063c8b10357394b6bba8cb1c10de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fzaninotto/Faker/zipball/f72816b43e74063c8b10357394b6bba8cb1c10de",
"reference": "f72816b43e74063c8b10357394b6bba8cb1c10de",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"ext-intl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7",
"squizlabs/php_codesniffer": "^1.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
},
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "François Zaninotto"
}
],
"description": "Faker is a PHP library that generates fake data for you.",
"keywords": [
"data",
"faker",
"fixtures"
],
"time": "2018-07-12T10:23:15+00:00"
},
{ {
"name": "jakub-onderka/php-console-color", "name": "jakub-onderka/php-console-color",
"version": "v0.2", "version": "v0.2",
@ -937,6 +987,48 @@
], ],
"time": "2019-02-16T20:54:15+00:00" "time": "2019-02-16T20:54:15+00:00"
}, },
{
"name": "openpsa/ranger",
"version": "v0.4.0",
"source": {
"type": "git",
"url": "https://github.com/flack/ranger.git",
"reference": "c0af25836e5923ee69f57ce4a8ff740b19ecfad6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/flack/ranger/zipball/c0af25836e5923ee69f57ce4a8ff740b19ecfad6",
"reference": "c0af25836e5923ee69f57ce4a8ff740b19ecfad6",
"shasum": ""
},
"require": {
"php": ">=5.4.0",
"symfony/intl": "~2.6|~3.4|~4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"OpenPsa\\Ranger\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Andreas Flack",
"email": "flack@contentcontrol-berlin.de",
"homepage": "http://www.contentcontrol-berlin.de/"
}
],
"description": "Formatter for date and time ranges with i18n support",
"keywords": [
"date range",
"time range"
],
"time": "2018-07-23T12:30:50+00:00"
},
{ {
"name": "opis/closure", "name": "opis/closure",
"version": "3.1.6", "version": "3.1.6",
@ -1393,6 +1485,93 @@
], ],
"time": "2018-07-19T23:38:55+00:00" "time": "2018-07-19T23:38:55+00:00"
}, },
{
"name": "reinink/advanced-eloquent",
"version": "v0.2.0",
"source": {
"type": "git",
"url": "https://github.com/reinink/advanced-eloquent.git",
"reference": "2d92b29ba085fd537c198868e8e5c15a0bf205b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/reinink/advanced-eloquent/zipball/2d92b29ba085fd537c198868e8e5c15a0bf205b8",
"reference": "2d92b29ba085fd537c198868e8e5c15a0bf205b8",
"shasum": ""
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"AdvancedEloquent\\ServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"AdvancedEloquent\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"homepage": "https://reinink.ca"
}
],
"description": "A set of advanced Eloquent macros for Laravel",
"homepage": "https://github.com/reinink/advanced-eloquent",
"keywords": [
"advanced",
"eloquent",
"laravel",
"macro"
],
"time": "2018-11-23T20:30:53+00:00"
},
{
"name": "reinink/inertia-laravel",
"version": "dev-master",
"dist": {
"type": "path",
"url": "../inertia-laravel",
"reference": "3a9577862dcd68e63a46a5b67068de007d6cea29",
"shasum": null
},
"require-dev": {
"orchestra/testbench": "~3.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Inertia\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Jonathan Reinink",
"email": "jonathan@reinink.ca",
"homepage": "https://reinink.ca"
}
],
"description": "The Laravel adapter for Inertia.",
"keywords": [
"inertia",
"laravel"
]
},
{ {
"name": "swiftmailer/swiftmailer", "name": "swiftmailer/swiftmailer",
"version": "v6.1.3", "version": "v6.1.3",
@ -1957,6 +2136,81 @@
"homepage": "https://symfony.com", "homepage": "https://symfony.com",
"time": "2019-03-03T19:38:09+00:00" "time": "2019-03-03T19:38:09+00:00"
}, },
{
"name": "symfony/intl",
"version": "v4.2.4",
"source": {
"type": "git",
"url": "https://github.com/symfony/intl.git",
"reference": "b2af5ce379781fd4811f79746512fc1934333fbb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/intl/zipball/b2af5ce379781fd4811f79746512fc1934333fbb",
"reference": "b2af5ce379781fd4811f79746512fc1934333fbb",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"symfony/polyfill-intl-icu": "~1.0"
},
"require-dev": {
"symfony/filesystem": "~3.4|~4.0"
},
"suggest": {
"ext-intl": "to use the component with locales other than \"en\""
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.2-dev"
}
},
"autoload": {
"psr-4": {
"Symfony\\Component\\Intl\\": ""
},
"classmap": [
"Resources/stubs"
],
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Bernhard Schussek",
"email": "bschussek@gmail.com"
},
{
"name": "Eriksen Costa",
"email": "eriksen.costa@infranology.com.br"
},
{
"name": "Igor Wiedler",
"email": "igor@wiedler.ch"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A PHP replacement layer for the C intl extension that includes additional data from the ICU library.",
"homepage": "https://symfony.com",
"keywords": [
"i18n",
"icu",
"internationalization",
"intl",
"l10n",
"localization"
],
"time": "2019-02-23T15:17:42+00:00"
},
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.10.0", "version": "v1.10.0",
@ -2015,6 +2269,64 @@
], ],
"time": "2018-08-06T14:22:27+00:00" "time": "2018-08-06T14:22:27+00:00"
}, },
{
"name": "symfony/polyfill-intl-icu",
"version": "v1.10.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-icu.git",
"reference": "f22a90256d577c7ef7efad8df1f0201663d57644"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/f22a90256d577c7ef7efad8df1f0201663d57644",
"reference": "f22a90256d577c7ef7efad8df1f0201663d57644",
"shasum": ""
},
"require": {
"php": ">=5.3.3",
"symfony/intl": "~2.3|~3.0|~4.0"
},
"suggest": {
"ext-intl": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for intl's ICU-related data and classes",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"icu",
"intl",
"polyfill",
"portable",
"shim"
],
"time": "2018-08-06T14:22:27+00:00"
},
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.10.0", "version": "v1.10.0",
@ -2404,6 +2716,57 @@
], ],
"time": "2019-02-23T15:17:42+00:00" "time": "2019-02-23T15:17:42+00:00"
}, },
{
"name": "tightenco/ziggy",
"version": "v0.6.9",
"source": {
"type": "git",
"url": "https://github.com/tightenco/ziggy.git",
"reference": "0f59b703236f6b19002bc28d1d3eafcbf479bd43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tightenco/ziggy/zipball/0f59b703236f6b19002bc28d1d3eafcbf479bd43",
"reference": "0f59b703236f6b19002bc28d1d3eafcbf479bd43",
"shasum": ""
},
"require": {
"laravel/framework": "~5.4"
},
"require-dev": {
"mikey179/vfsstream": "^1.6",
"orchestra/testbench": "~3.6"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Tightenco\\Ziggy\\ZiggyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Tightenco\\Ziggy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matt Stauffer",
"email": "matt@tighten.co"
},
{
"name": "Daniel Coulbourne",
"email": "daniel@tighten.co"
}
],
"description": "Generates a Blade directive exporting all of your named Laravel routes. Also provides a nice route() helper function in JavaScript.",
"time": "2018-10-29T06:06:46+00:00"
},
{ {
"name": "tijsverkoyen/css-to-inline-styles", "name": "tijsverkoyen/css-to-inline-styles",
"version": "2.2.1", "version": "2.2.1",
@ -2453,16 +2816,16 @@
}, },
{ {
"name": "vlucas/phpdotenv", "name": "vlucas/phpdotenv",
"version": "v3.3.2", "version": "v3.3.3",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/vlucas/phpdotenv.git", "url": "https://github.com/vlucas/phpdotenv.git",
"reference": "1ee9369cfbf26cfcf1f2515d98f15fab54e9647a" "reference": "dbcc609971dd9b55f48b8008b553d79fd372ddde"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/1ee9369cfbf26cfcf1f2515d98f15fab54e9647a", "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/dbcc609971dd9b55f48b8008b553d79fd372ddde",
"reference": "1ee9369cfbf26cfcf1f2515d98f15fab54e9647a", "reference": "dbcc609971dd9b55f48b8008b553d79fd372ddde",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -2501,10 +2864,78 @@
"env", "env",
"environment" "environment"
], ],
"time": "2019-01-30T10:43:17+00:00" "time": "2019-03-06T09:39:45+00:00"
} }
], ],
"packages-dev": [ "packages-dev": [
{
"name": "barryvdh/laravel-debugbar",
"version": "v3.2.3",
"source": {
"type": "git",
"url": "https://github.com/barryvdh/laravel-debugbar.git",
"reference": "5fcba4cc8e92a230b13b99c1083fc22ba8a5c479"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/5fcba4cc8e92a230b13b99c1083fc22ba8a5c479",
"reference": "5fcba4cc8e92a230b13b99c1083fc22ba8a5c479",
"shasum": ""
},
"require": {
"illuminate/routing": "5.5.x|5.6.x|5.7.x|5.8.x",
"illuminate/session": "5.5.x|5.6.x|5.7.x|5.8.x",
"illuminate/support": "5.5.x|5.6.x|5.7.x|5.8.x",
"maximebf/debugbar": "~1.15.0",
"php": ">=7.0",
"symfony/debug": "^3|^4",
"symfony/finder": "^3|^4"
},
"require-dev": {
"laravel/framework": "5.5.x"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.2-dev"
},
"laravel": {
"providers": [
"Barryvdh\\Debugbar\\ServiceProvider"
],
"aliases": {
"Debugbar": "Barryvdh\\Debugbar\\Facade"
}
}
},
"autoload": {
"psr-4": {
"Barryvdh\\Debugbar\\": "src/"
},
"files": [
"src/helpers.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "PHP Debugbar integration for Laravel",
"keywords": [
"debug",
"debugbar",
"laravel",
"profiler",
"webprofiler"
],
"time": "2019-02-26T18:01:54+00:00"
},
{ {
"name": "beyondcode/laravel-dump-server", "name": "beyondcode/laravel-dump-server",
"version": "1.2.2", "version": "1.2.2",
@ -2681,56 +3112,6 @@
], ],
"time": "2018-10-23T09:00:00+00:00" "time": "2018-10-23T09:00:00+00:00"
}, },
{
"name": "fzaninotto/faker",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/fzaninotto/Faker.git",
"reference": "f72816b43e74063c8b10357394b6bba8cb1c10de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fzaninotto/Faker/zipball/f72816b43e74063c8b10357394b6bba8cb1c10de",
"reference": "f72816b43e74063c8b10357394b6bba8cb1c10de",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
},
"require-dev": {
"ext-intl": "*",
"phpunit/phpunit": "^4.8.35 || ^5.7",
"squizlabs/php_codesniffer": "^1.5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
},
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "François Zaninotto"
}
],
"description": "Faker is a PHP library that generates fake data for you.",
"keywords": [
"data",
"faker",
"fixtures"
],
"time": "2018-07-12T10:23:15+00:00"
},
{ {
"name": "hamcrest/hamcrest-php", "name": "hamcrest/hamcrest-php",
"version": "v2.0.0", "version": "v2.0.0",
@ -2779,6 +3160,67 @@
], ],
"time": "2016-01-20T08:20:44+00:00" "time": "2016-01-20T08:20:44+00:00"
}, },
{
"name": "maximebf/debugbar",
"version": "v1.15.0",
"source": {
"type": "git",
"url": "https://github.com/maximebf/php-debugbar.git",
"reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/30e7d60937ee5f1320975ca9bc7bcdd44d500f07",
"reference": "30e7d60937ee5f1320975ca9bc7bcdd44d500f07",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"psr/log": "^1.0",
"symfony/var-dumper": "^2.6|^3.0|^4.0"
},
"require-dev": {
"phpunit/phpunit": "^4.0|^5.0"
},
"suggest": {
"kriswallsmith/assetic": "The best way to manage assets",
"monolog/monolog": "Log using Monolog",
"predis/predis": "Redis storage"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.14-dev"
}
},
"autoload": {
"psr-4": {
"DebugBar\\": "src/DebugBar/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maxime Bouroumeau-Fuseau",
"email": "maxime.bouroumeau@gmail.com",
"homepage": "http://maximebf.com"
},
{
"name": "Barry vd. Heuvel",
"email": "barryvdh@gmail.com"
}
],
"description": "Debug bar in the browser for php application",
"homepage": "https://github.com/maximebf/php-debugbar",
"keywords": [
"debug",
"debugbar"
],
"time": "2017-12-15T11:13:46+00:00"
},
{ {
"name": "mockery/mockery", "name": "mockery/mockery",
"version": "1.2.2", "version": "1.2.2",
@ -4269,11 +4711,14 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "dev", "minimum-stability": "dev",
"stability-flags": [], "stability-flags": {
"reinink/inertia-laravel": 20
},
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": "^7.1.3" "php": "~7.2.0",
"ext-redis": "*"
}, },
"platform-dev": [] "platform-dev": []
} }

View File

@ -0,0 +1,17 @@
<?php
use Faker\Generator as Faker;
$factory->define(App\Contact::class, function (Faker $faker) {
return [
'first_name' => $faker->firstName,
'last_name' => $faker->lastName,
'email' => $faker->unique()->safeEmail,
'phone' => $faker->tollFreePhoneNumber,
'address' => $faker->streetAddress,
'city' => $faker->city,
'region' => $faker->state,
'country' => 'US',
'postal_code' => $faker->postcode,
];
});

View File

@ -0,0 +1,16 @@
<?php
use Faker\Generator as Faker;
$factory->define(App\Organization::class, function (Faker $faker) {
return [
'name' => $faker->company,
'email' => $faker->companyEmail,
'phone' => $faker->tollFreePhoneNumber,
'address' => $faker->streetAddress,
'city' => $faker->city,
'region' => $faker->state,
'country' => 'US',
'postal_code' => $faker->postcode,
];
});

View File

@ -1,7 +1,5 @@
<?php <?php
use App\User;
use Illuminate\Support\Str;
use Faker\Generator as Faker; use Faker\Generator as Faker;
/* /*
@ -15,12 +13,13 @@ use Faker\Generator as Faker;
| |
*/ */
$factory->define(User::class, function (Faker $faker) { $factory->define(App\User::class, function (Faker $faker) {
return [ return [
'name' => $faker->name, 'first_name' => $faker->firstName,
'last_name' => $faker->lastName,
'email' => $faker->unique()->safeEmail, 'email' => $faker->unique()->safeEmail,
'email_verified_at' => now(), 'password' => 'secret',
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password 'remember_token' => str_random(10),
'remember_token' => Str::random(10), 'owner' => false,
]; ];
}); });

View File

@ -1,36 +0,0 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('users');
}
}

View File

@ -0,0 +1,17 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateAccountsTable extends Migration
{
public function up()
{
Schema::create('accounts', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 50);
$table->timestamps();
});
}
}

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateContactsTable extends Migration
{
public function up()
{
Schema::create('contacts', function (Blueprint $table) {
$table->increments('id');
$table->integer('account_id')->index();
$table->integer('organization_id')->nullable()->index();
$table->string('first_name', 25);
$table->string('last_name', 25);
$table->string('email', 50)->nullable();
$table->string('phone', 50)->nullable();
$table->string('address', 150)->nullable();
$table->string('city', 50)->nullable();
$table->string('region', 50)->nullable();
$table->string('country', 2)->nullable();
$table->string('postal_code', 25)->nullable();
$table->timestamps();
$table->softDeletes();
});
}
}

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateOrganizationsTable extends Migration
{
public function up()
{
Schema::create('organizations', function (Blueprint $table) {
$table->increments('id');
$table->integer('account_id')->index();
$table->string('name', 100);
$table->string('email', 50)->nullable();
$table->string('phone', 50)->nullable();
$table->string('address', 150)->nullable();
$table->string('city', 50)->nullable();
$table->string('region', 50)->nullable();
$table->string('country', 2)->nullable();
$table->string('postal_code', 25)->nullable();
$table->timestamps();
$table->softDeletes();
});
}
}

View File

@ -6,11 +6,6 @@ use Illuminate\Database\Migrations\Migration;
class CreatePasswordResetsTable extends Migration class CreatePasswordResetsTable extends Migration
{ {
/**
* Run the migrations.
*
* @return void
*/
public function up() public function up()
{ {
Schema::create('password_resets', function (Blueprint $table) { Schema::create('password_resets', function (Blueprint $table) {
@ -19,14 +14,4 @@ class CreatePasswordResetsTable extends Migration
$table->timestamp('created_at')->nullable(); $table->timestamp('created_at')->nullable();
}); });
} }
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('password_resets');
}
} }

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateUsersTable extends Migration
{
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->integer('account_id')->index();
$table->string('first_name', 25);
$table->string('last_name', 25);
$table->string('email', 50)->unique();
$table->string('password')->nullable();
$table->boolean('owner')->default(false);
$table->rememberToken();
$table->timestamps();
$table->softDeletes();
});
}
}

View File

@ -1,16 +1,34 @@
<?php <?php
use App\User;
use App\Account;
use App\Contact;
use App\Organization;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
/**
* Seed the application's database.
*
* @return void
*/
public function run() public function run()
{ {
// $this->call(UsersTableSeeder::class); $account = Account::create(['name' => 'Acme Corporation']);
factory(User::class)->create([
'account_id' => $account->id,
'first_name' => 'Jonathan',
'last_name' => 'Reinink',
'email' => 'jonathan@reinink.ca',
'owner' => true,
]);
factory(User::class, 5)->create(['account_id' => $account->id]);
$organizations = factory(Organization::class, 100)
->create(['account_id' => $account->id]);
factory(Contact::class, 100)
->create(['account_id' => $account->id])
->each(function ($contact) use ($organizations) {
$contact->update(['organization_id' => $organizations->random()->id]);
});
} }
} }

10119
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -10,16 +10,23 @@
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
"autosize": "^4.0.2",
"axios": "^0.18", "axios": "^0.18",
"bootstrap": "^4.0.0",
"cross-env": "^5.1", "cross-env": "^5.1",
"jquery": "^3.2", "eslint": "^5.14.1",
"eslint-plugin-vue": "^5.2.2",
"fuse.js": "^3.4.2",
"inertia-vue": "file:../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",
"portal-vue": "^1.5.1",
"postcss-import": "^12.0.1",
"postcss-nesting": "^7.0.0",
"resolve-url-loader": "^2.3.1", "resolve-url-loader": "^2.3.1",
"sass": "^1.15.2", "tailwindcss": "^0.7.4",
"sass-loader": "^7.1.0", "vue": "^2.6.6",
"vue": "^2.5.17" "vue-template-compiler": "^2.6.6"
} }
} }

8
public/css/app.css vendored

File diff suppressed because one or more lines are too long

1
public/js/app.js vendored

File diff suppressed because one or more lines are too long

12
resources/css/app.css vendored Normal file
View File

@ -0,0 +1,12 @@
/* Reset */
@import "tailwindcss/preflight";
@import "reset";
/* Components */
@import "buttons";
@import "form";
@import "nprogress";
@import "spinner";
/* Utilities */
@import "tailwindcss/utilities";

5
resources/css/buttons.css vendored Normal file
View File

@ -0,0 +1,5 @@
.btn-indigo {
@apply px-6 py-3 rounded bg-indigo-dark text-white text-sm font-bold whitespace-no-wrap;
&:hover, &:focus { @apply bg-orange }
}

46
resources/css/form.css vendored Normal file
View File

@ -0,0 +1,46 @@
.form-label {
@apply .mb-2 .block .text-grey-darkest .select-none;
}
.form-input,
.form-textarea,
.form-select {
@apply .p-2 .leading-normal .block .w-full .border .text-grey-darkest .bg-white .font-sans .rounded .text-left .appearance-none .relative;
&:focus,
&.focus {
@apply .border-indigo;
box-shadow: 0 0 0 1px config('colors.indigo');
}
&::placeholder {
@apply .text-grey-dark .opacity-100;
}
}
.form-select {
@apply .pr-6;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAQCAYAAAAMJL+VAAAABGdBTUEAALGPC/xhBQAAAQtJREFUOBG1lEEOgjAQRalbGj2OG9caOACn4ALGtfEuHACiazceR1PWOH/CNA3aMiTaBDpt/7zPdBKy7M/DCL9pGkvxxVp7KsvyJftL5rZt1865M+Ucq6pyyF3hNcI7Cuu+728QYn/JQA5yKaempxuZmQngOwEaYx55nu+1lQh8GIatMGi+01NwBcEmhxBqK4nAPZJ78K0KKFAJmR3oPp8+Iwgob0Oa6+TLoeCvRx+mTUYf/FVBGTPRwDkfLxnaSrRwcH0FWhNOmrkWYbE2XEicqgSa1J0LQ+aPCuQgZiLnwewbGuz5MGoAhcIkCQcjaTBjMgtXGURMVHC1wcQEy0J+Zlj8bKAnY1/UzDe2dbAVqfXn6wAAAABJRU5ErkJggg==');
background-size: 0.7rem;
background-repeat: no-repeat;
background-position: right 0.7rem center;
&::-ms-expand {
@apply .opacity-0;
}
}
.form-error {
@apply .text-red .mt-2 .text-sm;
}
.form-input.error,
.form-textarea.error,
.form-select.error {
@apply .border-red-light;
&:focus {
box-shadow: 0 0 0 1px config('colors.red');
}
}

73
resources/css/nprogress.css vendored Normal file
View File

@ -0,0 +1,73 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #29d;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
opacity: 1.0;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: block;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #29d;
border-left-color: #29d;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% { -webkit-transform: rotate(0deg); }
100% { -webkit-transform: rotate(360deg); }
}
@keyframes nprogress-spinner {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}

10
resources/css/reset.css vendored Normal file
View File

@ -0,0 +1,10 @@
a {
color: inherit;
text-decoration: none;
}
input, select, textarea, button, div, a {
&:focus, &:active {
outline: none;
}
}

32
resources/css/spinner.css vendored Normal file
View File

@ -0,0 +1,32 @@
.spinner,
.spinner:after {
border-radius: 50%;
width: 1.5em;
height: 1.5em;
}
.spinner {
font-size: 10px;
position: relative;
text-indent: -9999em;
border-top: .2em solid white;
border-right: .2em solid white;
border-bottom: .2em solid white;
border-left: .2em solid transparent;
-webkit-transform: translateZ(0);
-ms-transform: translateZ(0);
transform: translateZ(0);
-webkit-animation: loading 1s infinite linear;
animation: loading 1s infinite linear;
}
@keyframes loading {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,63 @@
<template>
<div class="p-6 bg-indigo-darker min-h-screen flex justify-center items-center">
<div class="w-full max-w-sm">
<logo class="block mx-auto w-full max-w-xs fill-white" height="50" />
<form class="mt-8 bg-white rounded-lg shadow-lg overflow-hidden" @submit.prevent="submit">
<div class="px-10 py-12">
<h1 class="text-center font-bold text-3xl">Welcome Back!</h1>
<div class="mx-auto mt-6 w-24 border-b-2" />
<text-input v-model="form.fields.email" class="mt-10" label="Email" :error="form.errors.first('email')" type="email" autofocus autocapitalize="off" />
<text-input v-model="form.fields.password" class="mt-6" label="Password" :error="form.errors.first('password')" type="password" />
<label class="mt-6 select-none flex items-center" for="remember">
<input id="remember" v-model="form.fields.remember" class="mr-1" type="checkbox">
<span class="text-sm">Remember Me</span>
</label>
</div>
<div class="px-10 py-4 bg-grey-lightest border-t border-grey-lighter flex justify-between items-center">
<a class="hover:underline" tabindex="-1" href="#reset-password">Forget password?</a>
<loading-button :loading="form.sending" class="btn-indigo" type="submit">Login</loading-button>
</div>
</form>
</div>
</div>
</template>
<script>
import { Inertia } from 'inertia-vue'
import Form from '@/Utils/Form'
import LoadingButton from '@/Shared/LoadingButton'
import Logo from '@/Shared/Logo'
import TextInput from '@/Shared/TextInput'
export default {
components: {
LoadingButton,
Logo,
TextInput,
},
props: {
intendedUrl: String,
},
inject: ['page'],
data() {
return {
form: new Form({
email: null,
password: null,
remember: null,
}),
}
},
mounted() {
document.title = `Login | ${this.page.props.app.name}`
},
methods: {
submit() {
this.form.post({
url: this.route('login.attempt').url(),
then: () => Inertia.visit(this.intendedUrl),
})
},
},
}
</script>

View File

@ -0,0 +1,80 @@
<template>
<layout title="Create Contact">
<h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-light hover:text-indigo-dark" :href="route('contacts')">Contacts</inertia-link>
<span class="text-indigo-light font-medium">/</span> Create
</h1>
<div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.fields.first_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('first_name')" label="First name" />
<text-input v-model="form.fields.last_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('last_name')" label="Last name" />
<select-input v-model="form.fields.organization_id" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('organization_id')" label="Organization">
<option :value="null" />
<option v-for="organization in organizations" :key="organization.id" :value="organization.id">{{ organization.name }}</option>
</select-input>
<text-input v-model="form.fields.email" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('email')" label="Email" />
<text-input v-model="form.fields.phone" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('phone')" label="Phone" />
<text-input v-model="form.fields.address" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('address')" label="Address" />
<text-input v-model="form.fields.city" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('city')" label="City" />
<text-input v-model="form.fields.region" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('region')" label="Province/State" />
<select-input v-model="form.fields.country" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('country')" label="Country">
<option :value="null" />
<option value="CA">Canada</option>
<option value="US">United States</option>
</select-input>
<text-input v-model="form.fields.postal_code" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('postal_code')" label="Postal code" />
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex justify-end items-center">
<loading-button :loading="form.sending" class="btn-indigo" type="submit">Create Contact</loading-button>
</div>
</form>
</div>
</layout>
</template>
<script>
import { Inertia, InertiaLink } from 'inertia-vue'
import Form from '@/Utils/Form'
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
export default {
components: {
InertiaLink,
Layout,
LoadingButton,
SelectInput,
TextInput,
},
props: {
organizations: Array,
},
data() {
return {
form: new Form({
first_name: null,
last_name: null,
organization_id: null,
email: null,
phone: null,
address: null,
city: null,
region: null,
country: null,
postal_code: null,
}),
}
},
methods: {
submit() {
this.form.post({
url: this.route('contacts.store').url(),
then: data => Inertia.visit(this.route('contacts.edit', data.id)),
})
},
},
}
</script>

View File

@ -0,0 +1,104 @@
<template>
<layout :title="`${form.fields.first_name} ${form.fields.last_name}`">
<h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-light hover:text-indigo-dark" :href="route('contacts')">Contacts</inertia-link>
<span class="text-indigo-light font-medium">/</span>
{{ form.fields.first_name }} {{ form.fields.last_name }}
</h1>
<trashed-message v-if="contact.deleted_at" class="mb-6" @restore="restore">
This contact has been deleted.
</trashed-message>
<div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.fields.first_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('first_name')" label="First name" />
<text-input v-model="form.fields.last_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('last_name')" label="Last name" />
<select-input v-model="form.fields.organization_id" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('organization_id')" label="Organization">
<option :value="null" />
<option v-for="organization in organizations" :key="organization.id" :value="organization.id">{{ organization.name }}</option>
</select-input>
<text-input v-model="form.fields.email" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('email')" label="Email" />
<text-input v-model="form.fields.phone" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('phone')" label="Phone" />
<text-input v-model="form.fields.address" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('address')" label="Address" />
<text-input v-model="form.fields.city" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('city')" label="City" />
<text-input v-model="form.fields.region" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('region')" label="Province/State" />
<select-input v-model="form.fields.country" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('country')" label="Country">
<option :value="null" />
<option value="CA">Canada</option>
<option value="US">United States</option>
</select-input>
<text-input v-model="form.fields.postal_code" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('postal_code')" label="Postal code" />
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex items-center">
<button v-if="!contact.deleted_at" class="text-red hover:underline" tabindex="-1" type="button" @click="destroy">Delete Contact</button>
<loading-button :loading="form.sending" class="btn-indigo ml-auto" type="submit">Update Contact</loading-button>
</div>
</form>
</div>
</layout>
</template>
<script>
import { Inertia, InertiaLink } from 'inertia-vue'
import Form from '@/Utils/Form'
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
import TrashedMessage from '@/Shared/TrashedMessage'
export default {
components: {
InertiaLink,
Layout,
LoadingButton,
SelectInput,
TextInput,
TrashedMessage,
},
props: {
contact: Object,
organizations: Array,
},
data() {
return {
form: new Form({
first_name: this.contact.first_name,
last_name: this.contact.last_name,
organization_id: this.contact.organization_id,
email: this.contact.email,
phone: this.contact.phone,
address: this.contact.address,
city: this.contact.city,
region: this.contact.region,
country: this.contact.country,
postal_code: this.contact.postal_code,
}),
}
},
methods: {
submit() {
this.form.put({
url: this.route('contacts.update', this.contact.id).url(),
then: () => Inertia.visit(this.route('contacts')),
})
},
destroy() {
if (confirm('Are you sure you want to delete this contact?')) {
this.form.delete({
url: this.route('contacts.destroy', this.contact.id).url(),
then: () => Inertia.replace(this.route('contacts.edit', this.contact.id).url()),
})
}
},
restore() {
if (confirm('Are you sure you want to restore this contact?')) {
this.form.put({
url: this.route('contacts.restore', this.contact.id).url(),
then: () => Inertia.replace(this.route('contacts.edit', this.contact.id).url()),
})
}
},
},
}
</script>

View File

@ -0,0 +1,108 @@
<template>
<layout title="Contacts">
<h1 class="mb-8 font-bold text-3xl">Contacts</h1>
<div class="mb-6 flex justify-between items-center">
<search-filter v-model="form.search" class="w-full max-w-sm mr-4" @reset="reset">
<label class="block text-grey-darkest">Trashed:</label>
<select v-model="form.trashed" class="mt-1 w-full form-select">
<option :value="null" />
<option value="with">With Trashed</option>
<option value="only">Only Trashed</option>
</select>
</search-filter>
<inertia-link class="btn-indigo" :href="route('contacts.create')">
<span>Create</span>
<span class="hidden md:inline">Contact</span>
</inertia-link>
</div>
<div class="bg-white rounded shadow overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<tr class="text-left font-bold">
<th class="px-6 pt-6 pb-4">Name</th>
<th class="px-6 pt-6 pb-4">Organization</th>
<th class="px-6 pt-6 pb-4">City</th>
<th class="px-6 pt-6 pb-4" colspan="2">Phone</th>
</tr>
<tr v-for="contact in contacts.data" :key="contact.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('contacts.edit', contact.id)">
{{ contact.name }}
<icon v-if="contact.deleted_at" name="trash" class="flex-no-shrink w-3 h-3 fill-grey ml-2" />
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
<div v-if="contact.organization">
{{ contact.organization.name }}
</div>
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
{{ contact.city }}
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
{{ contact.phone }}
</inertia-link>
</td>
<td class="border-t w-px">
<inertia-link class="px-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
<icon name="cheveron-right" class="block w-6 h-6 fill-grey" />
</inertia-link>
</td>
</tr>
<tr v-if="contacts.data.length === 0">
<td class="border-t px-6 py-4" colspan="4">No contacts found.</td>
</tr>
</table>
</div>
<pagination :links="contacts.links" />
</layout>
</template>
<script>
import _ from 'lodash'
import { Inertia, InertiaLink } from 'inertia-vue'
import Icon from '@/Shared/Icon'
import Layout from '@/Shared/Layout'
import Pagination from '@/Shared/Pagination'
import SearchFilter from '@/Shared/SearchFilter'
export default {
components: {
InertiaLink,
Icon,
Layout,
Pagination,
SearchFilter,
},
props: {
contacts: Object,
filters: Object,
},
data() {
return {
form: {
search: this.filters.search,
trashed: this.filters.trashed,
},
}
},
watch: {
form: {
handler: _.throttle(function() {
let query = _.pickBy(this.form)
Inertia.replace(this.route('contacts', Object.keys(query).length ? query : { remember: 'forget' }).url())
}, 150),
deep: true,
},
},
methods: {
reset() {
this.form = _.mapValues(this.form, () => null)
},
},
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<layout title="Dashboard">
<h1 class="mb-8 font-bold text-3xl">Dashboard</h1>
<div class="mt-16">
<inertia-link class="btn-indigo" href="/500">500 error</inertia-link>
<inertia-link class="btn-indigo" href="/404">404 error</inertia-link>
</div>
</layout>
</template>
<script>
import { InertiaLink } from 'inertia-vue'
import Layout from '@/Shared/Layout'
export default {
components: {
Layout,
InertiaLink,
},
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<layout title="Create Organization">
<h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-light hover:text-indigo-dark" :href="route('organizations')">Organizations</inertia-link>
<span class="text-indigo-light font-medium">/</span> Create
</h1>
<div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.fields.name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('name')" label="Name" />
<text-input v-model="form.fields.email" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('email')" label="Email" />
<text-input v-model="form.fields.phone" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('phone')" label="Phone" />
<text-input v-model="form.fields.address" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('address')" label="Address" />
<text-input v-model="form.fields.city" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('city')" label="City" />
<text-input v-model="form.fields.region" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('region')" label="Province/State" />
<select-input v-model="form.fields.country" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('country')" label="Country">
<option :value="null" />
<option value="CA">Canada</option>
<option value="US">United States</option>
</select-input>
<text-input v-model="form.fields.postal_code" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('postal_code')" label="Postal code" />
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex justify-end items-center">
<loading-button :loading="form.sending" class="btn-indigo" type="submit">Create Organization</loading-button>
</div>
</form>
</div>
</layout>
</template>
<script>
import { Inertia, InertiaLink } from 'inertia-vue'
import Form from '@/Utils/Form'
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
export default {
components: {
InertiaLink,
Layout,
LoadingButton,
SelectInput,
TextInput,
},
data() {
return {
form: new Form({
name: null,
email: null,
phone: null,
address: null,
city: null,
region: null,
country: null,
postal_code: null,
}),
}
},
methods: {
submit() {
this.form.post({
url: this.route('organizations.store').url(),
then: data => Inertia.visit(this.route('organizations.edit', data.id)),
})
},
},
}
</script>

View File

@ -0,0 +1,134 @@
<template>
<layout :title="form.fields.name">
<h1 class="mb-8 font-bold text-3xl">
<inertia-link class="text-indigo-light hover:text-indigo-dark" :href="route('organizations')">Organizations</inertia-link>
<span class="text-indigo-light font-medium">/</span>
{{ form.fields.name }}
</h1>
<trashed-message v-if="organization.deleted_at" class="mb-6" @restore="restore">
This organization has been deleted.
</trashed-message>
<div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.fields.name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('name')" label="Name" />
<text-input v-model="form.fields.email" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('email')" label="Email" />
<text-input v-model="form.fields.phone" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('phone')" label="Phone" />
<text-input v-model="form.fields.address" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('address')" label="Address" />
<text-input v-model="form.fields.city" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('city')" label="City" />
<text-input v-model="form.fields.region" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('region')" label="Province/State" />
<select-input v-model="form.fields.country" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('country')" label="Country">
<option :value="null" />
<option value="CA">Canada</option>
<option value="US">United States</option>
</select-input>
<text-input v-model="form.fields.postal_code" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('postal_code')" label="Postal code" />
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex items-center">
<button v-if="!organization.deleted_at" class="text-red hover:underline" tabindex="-1" type="button" @click="destroy">Delete Organization</button>
<loading-button :loading="form.sending" class="btn-indigo ml-auto" type="submit">Update Organization</loading-button>
</div>
</form>
</div>
<h2 class="mt-12 font-bold text-2xl">Contacts</h2>
<div class="mt-6 bg-white rounded shadow overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<tr class="text-left font-bold">
<th class="px-6 pt-6 pb-4">Name</th>
<th class="px-6 pt-6 pb-4">City</th>
<th class="px-6 pt-6 pb-4" colspan="2">Phone</th>
</tr>
<tr v-for="contact in organization.contacts" :key="contact.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('contacts.edit', contact.id)">
{{ contact.name }}
<icon v-if="contact.deleted_at" name="trash" class="flex-no-shrink w-3 h-3 fill-grey ml-2" />
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
{{ contact.city }}
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
{{ contact.phone }}
</inertia-link>
</td>
<td class="border-t w-px">
<inertia-link class="px-4 flex items-center" :href="route('contacts.edit', contact.id)" tabindex="-1">
<icon name="cheveron-right" class="block w-6 h-6 fill-grey" />
</inertia-link>
</td>
</tr>
<tr v-if="organization.contacts.length === 0">
<td class="border-t px-6 py-4" colspan="4">No contacts found.</td>
</tr>
</table>
</div>
</layout>
</template>
<script>
import { Inertia, InertiaLink } from 'inertia-vue'
import Form from '@/Utils/Form'
import Icon from '@/Shared/Icon'
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
import TrashedMessage from '@/Shared/TrashedMessage'
export default {
components: {
InertiaLink,
Icon,
Layout,
LoadingButton,
SelectInput,
TextInput,
TrashedMessage,
},
props: {
organization: Object,
},
data() {
return {
form: new Form({
name: this.organization.name,
email: this.organization.email,
phone: this.organization.phone,
address: this.organization.address,
city: this.organization.city,
region: this.organization.region,
country: this.organization.country,
postal_code: this.organization.postal_code,
}),
}
},
methods: {
submit() {
this.form.put({
url: this.route('organizations.update', this.organization.id).url(),
then: () => Inertia.visit(this.route('organizations')),
})
},
destroy() {
if (confirm('Are you sure you want to delete this organization?')) {
this.form.delete({
url: this.route('organizations.destroy', this.organization.id).url(),
then: () => Inertia.replace(this.route('organizations.edit', this.organization.id).url()),
})
}
},
restore() {
if (confirm('Are you sure you want to restore this organization?')) {
this.form.put({
url: this.route('organizations.restore', this.organization.id).url(),
then: () => Inertia.replace(this.route('organizations.edit', this.organization.id).url()),
})
}
},
},
}
</script>

View File

@ -0,0 +1,100 @@
<template>
<layout title="Organizations">
<h1 class="mb-8 font-bold text-3xl">Organizations</h1>
<div class="mb-6 flex justify-between items-center">
<search-filter v-model="form.search" class="w-full max-w-sm mr-4" @reset="reset">
<label class="block text-grey-darkest">Trashed:</label>
<select v-model="form.trashed" class="mt-1 w-full form-select">
<option :value="null" />
<option value="with">With Trashed</option>
<option value="only">Only Trashed</option>
</select>
</search-filter>
<inertia-link class="btn-indigo" :href="route('organizations.create')">
<span>Create</span>
<span class="hidden md:inline">Organization</span>
</inertia-link>
</div>
<div class="bg-white rounded shadow overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<tr class="text-left font-bold">
<th class="px-6 pt-6 pb-4">Name</th>
<th class="px-6 pt-6 pb-4">City</th>
<th class="px-6 pt-6 pb-4" colspan="2">Phone</th>
</tr>
<tr v-for="organization in organizations.data" :key="organization.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('organizations.edit', organization.id)">
{{ organization.name }}
<icon v-if="organization.deleted_at" name="trash" class="flex-no-shrink w-3 h-3 fill-grey ml-2" />
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('organizations.edit', organization.id)" tabindex="-1">
{{ organization.city }}
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('organizations.edit', organization.id)" tabindex="-1">
{{ organization.phone }}
</inertia-link>
</td>
<td class="border-t w-px">
<inertia-link class="px-4 flex items-center" :href="route('organizations.edit', organization.id)" tabindex="-1">
<icon name="cheveron-right" class="block w-6 h-6 fill-grey" />
</inertia-link>
</td>
</tr>
<tr v-if="organizations.data.length === 0">
<td class="border-t px-6 py-4" colspan="4">No organizations found.</td>
</tr>
</table>
</div>
<pagination :links="organizations.links" />
</layout>
</template>
<script>
import _ from 'lodash'
import { Inertia, InertiaLink } from 'inertia-vue'
import Icon from '@/Shared/Icon'
import Layout from '@/Shared/Layout'
import Pagination from '@/Shared/Pagination'
import SearchFilter from '@/Shared/SearchFilter'
export default {
components: {
InertiaLink,
Icon,
Layout,
Pagination,
SearchFilter,
},
props: {
organizations: Object,
filters: Object,
},
data() {
return {
form: {
search: this.filters.search,
trashed: this.filters.trashed,
},
}
},
watch: {
form: {
handler: _.throttle(function() {
let query = _.pickBy(this.form)
Inertia.replace(this.route('organizations', Object.keys(query).length ? query : { remember: 'forget' }).url())
}, 150),
deep: true,
},
},
methods: {
reset() {
this.form = _.mapValues(this.form, () => null)
},
},
}
</script>

View File

@ -0,0 +1,15 @@
<template>
<layout title="Reports">
<h1 class="mb-8 font-bold text-3xl">Reports</h1>
</layout>
</template>
<script>
import Layout from '@/Shared/Layout'
export default {
components: {
Layout,
},
}
</script>

View File

@ -0,0 +1,66 @@
<template>
<layout title="Create User">
<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> Create
</h1>
<div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.fields.first_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('first_name')" label="First name" />
<text-input v-model="form.fields.last_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('last_name')" label="Last name" />
<text-input v-model="form.fields.email" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('email')" label="Email" />
<text-input v-model="form.fields.password" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('password')" type="password" autocomplete="new-password" label="Password" />
<select-input v-model="form.fields.owner" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('owner')" label="Owner">
<option :value="true">Yes</option>
<option :value="false">No</option>
</select-input>
</div>
<div class="px-8 py-4 bg-grey-lightest border-t border-grey-lighter flex justify-end items-center">
<loading-button :loading="form.sending" class="btn-indigo" type="submit">Create User</loading-button>
</div>
</form>
</div>
</layout>
</template>
<script>
import { Inertia, InertiaLink } from 'inertia-vue'
import Form from '@/Utils/Form'
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
export default {
components: {
InertiaLink,
Layout,
LoadingButton,
SelectInput,
TextInput,
},
props: {
organizations: Array,
},
data() {
return {
form: new Form({
first_name: null,
last_name: null,
email: null,
password: null,
owner: null,
}),
}
},
methods: {
submit() {
this.form.post({
url: this.route('users.store').url(),
then: data => Inertia.visit(this.route('users.edit', data.id)),
})
},
},
}
</script>

View File

@ -0,0 +1,90 @@
<template>
<layout :title="`${form.fields.first_name} ${form.fields.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.fields.first_name }} {{ form.fields.last_name }}
</h1>
<trashed-message v-if="user.deleted_at" class="mb-6" @restore="restore">
This user has been deleted.
</trashed-message>
<div class="bg-white rounded shadow overflow-hidden max-w-lg">
<form @submit.prevent="submit">
<div class="p-8 -mr-6 -mb-8 flex flex-wrap">
<text-input v-model="form.fields.first_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('first_name')" label="First name" />
<text-input v-model="form.fields.last_name" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('last_name')" label="Last name" />
<text-input v-model="form.fields.email" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('email')" label="Email" />
<text-input v-model="form.fields.password" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('password')" type="password" autocomplete="new-password" label="Password" />
<select-input v-model="form.fields.owner" class="pr-6 pb-8 w-full lg:w-1/2" :error="form.errors.first('owner')" label="Owner">
<option :value="true">Yes</option>
<option :value="false">No</option>
</select-input>
</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>
<loading-button :loading="form.sending" class="btn-indigo ml-auto" type="submit">Update User</loading-button>
</div>
</form>
</div>
</layout>
</template>
<script>
import { Inertia, InertiaLink } from 'inertia-vue'
import Form from '@/Utils/Form'
import Layout from '@/Shared/Layout'
import LoadingButton from '@/Shared/LoadingButton'
import SelectInput from '@/Shared/SelectInput'
import TextInput from '@/Shared/TextInput'
import TrashedMessage from '@/Shared/TrashedMessage'
export default {
components: {
InertiaLink,
Layout,
LoadingButton,
SelectInput,
TextInput,
TrashedMessage,
},
props: {
user: Object,
organizations: Array,
},
data() {
return {
form: new Form({
first_name: this.user.first_name,
last_name: this.user.last_name,
email: this.user.email,
password: this.user.password,
owner: this.user.owner,
}),
}
},
methods: {
submit() {
this.form.put({
url: this.route('users.update', this.user.id).url(),
then: () => Inertia.visit(this.route('users')),
})
},
destroy() {
if (confirm('Are you sure you want to delete this user?')) {
this.form.delete({
url: this.route('users.destroy', this.user.id).url(),
then: () => Inertia.replace(this.route('users.edit', this.user.id).url()),
})
}
},
restore() {
if (confirm('Are you sure you want to restore this user?')) {
this.form.put({
url: this.route('users.restore', this.user.id).url(),
then: () => Inertia.replace(this.route('users.edit', this.user.id).url()),
})
}
},
},
}
</script>

View File

@ -0,0 +1,104 @@
<template>
<layout title="Users">
<h1 class="mb-8 font-bold text-3xl">Users</h1>
<div class="mb-6 flex justify-between items-center">
<search-filter v-model="form.search" class="w-full max-w-sm mr-4" @reset="reset">
<label class="block text-grey-darkest">Role:</label>
<select v-model="form.role" class="mt-1 w-full form-select">
<option :value="null" />
<option value="user">User</option>
<option value="owner">Owner</option>
</select>
<label class="mt-4 block text-grey-darkest">Trashed:</label>
<select v-model="form.trashed" class="mt-1 w-full form-select">
<option :value="null" />
<option value="with">With Trashed</option>
<option value="only">Only Trashed</option>
</select>
</search-filter>
<inertia-link class="btn-indigo" :href="route('users.create')">
<span>Create</span>
<span class="hidden md:inline">User</span>
</inertia-link>
</div>
<div class="bg-white rounded shadow overflow-x-auto">
<table class="w-full whitespace-no-wrap">
<tr class="text-left font-bold">
<th class="px-6 pt-6 pb-4">Name</th>
<th class="px-6 pt-6 pb-4">Email</th>
<th class="px-6 pt-6 pb-4" colspan="2">Role</th>
</tr>
<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)">
{{ user.name }}
<icon v-if="user.deleted_at" name="trash" class="flex-no-shrink w-3 h-3 fill-grey ml-2" />
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('users.edit', user.id)" tabindex="-1">
{{ user.email }}
</inertia-link>
</td>
<td class="border-t">
<inertia-link class="px-6 py-4 flex items-center" :href="route('users.edit', user.id)" tabindex="-1">
{{ user.owner ? 'Owner' : 'User' }}
</inertia-link>
</td>
<td class="border-t w-px">
<inertia-link class="px-4 flex items-center" :href="route('users.edit', user.id)" tabindex="-1">
<icon name="cheveron-right" class="block w-6 h-6 fill-grey" />
</inertia-link>
</td>
</tr>
<tr v-if="users.length === 0">
<td class="border-t px-6 py-4" colspan="4">No users found.</td>
</tr>
</table>
</div>
</layout>
</template>
<script>
import _ from 'lodash'
import { Inertia, InertiaLink } from 'inertia-vue'
import Icon from '@/Shared/Icon'
import Layout from '@/Shared/Layout'
import SearchFilter from '@/Shared/SearchFilter'
export default {
components: {
InertiaLink,
Icon,
Layout,
SearchFilter,
},
props: {
users: Array,
filters: Object,
},
data() {
return {
form: {
search: this.filters.search,
role: this.filters.role,
trashed: this.filters.trashed,
},
}
},
watch: {
form: {
handler: _.throttle(function() {
let query = _.pickBy(this.form)
Inertia.replace(this.route('users', Object.keys(query).length ? query : { remember: 'forget' }).url())
}, 150),
deep: true,
},
},
methods: {
reset() {
this.form = _.mapValues(this.form, () => null)
},
},
}
</script>

View File

@ -0,0 +1,66 @@
<template>
<button type="button" @click="toggle">
<slot />
<portal v-if="show" to="dropdown">
<div>
<div style="position: fixed; top: 0; right: 0; left: 0; bottom: 0; z-index: 99998; background: black; opacity: .2" @click="toggle" />
<div ref="dropdown" style="position: absolute; z-index: 99999;" @click.stop>
<slot name="dropdown" />
</div>
</div>
</portal>
</button>
</template>
<script>
import Popper from 'popper.js'
export default {
props: {
placement: {
type: String,
default: 'bottom-end',
},
boundary: {
type: String,
default: 'scrollParent',
},
},
data() {
return {
show: false,
}
},
watch: {
show(show) {
if (show) {
this.$nextTick(() => {
this.popper = new Popper(this.$el, this.$refs.dropdown, {
placement: this.placement,
modifiers: {
preventOverflow: { boundariesElement: this.boundary },
},
})
})
} else if (this.popper) {
setTimeout(() => this.popper.destroy(), 100)
}
},
},
mounted() {
document.addEventListener('keydown', (e) => {
if (e.keyCode === 27) {
this.close()
}
})
},
methods: {
close() {
this.show = false
},
toggle() {
this.show = !this.show
},
},
}
</script>

View File

@ -0,0 +1,22 @@
<template>
<svg v-if="name === 'apple'" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><g fill-rule="nonzero"><path d="M46.173 19.967C49.927-1.838 19.797-.233 14.538.21c-.429.035-.648.4-.483.8 2.004 4.825 14.168 31.66 32.118 18.957zm13.18 1.636c1.269-.891 1.35-1.614.047-2.453l-2.657-1.71c-.94-.607-1.685-.606-2.532.129-5.094 4.42-7.336 9.18-8.211 15.24 1.597.682 3.55.79 5.265.328 1.298-4.283 3.64-8.412 8.088-11.534z" /><path d="M88.588 67.75c9.65-27.532-13.697-45.537-35.453-32.322-1.84 1.118-4.601 1.118-6.441 0-21.757-13.215-45.105 4.79-35.454 32.321 5.302 15.123 17.06 39.95 37.295 29.995.772-.38 1.986-.38 2.758 0 20.235 9.955 31.991-14.872 37.295-29.995z" /></g></svg>
<svg v-else-if="name === 'book'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M6 4H5a1 1 0 1 1 0-2h11V1a1 1 0 0 0-1-1H4a2 2 0 0 0-2 2v16c0 1.1.9 2 2 2h12a2 2 0 0 0 2-2V5a1 1 0 0 0-1-1h-7v8l-2-2-2 2V4z" /></svg>
<svg v-else-if="name === 'cheveron-down'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" /></svg>
<svg v-else-if="name === 'cheveron-right'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><polygon points="12.95 10.707 13.657 10 8 4.343 6.586 5.757 10.828 10 6.586 14.243 8 15.657 12.95 10.707" /></svg>
<svg v-else-if="name === 'dashboard'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 20a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm-5.6-4.29a9.95 9.95 0 0 1 11.2 0 8 8 0 1 0-11.2 0zm6.12-7.64l3.02-3.02 1.41 1.41-3.02 3.02a2 2 0 1 1-1.41-1.41z" /></svg>
<svg v-else-if="name === 'location'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 20S3 10.87 3 7a7 7 0 1 1 14 0c0 3.87-7 13-7 13zm0-11a2 2 0 1 0 0-4 2 2 0 0 0 0 4z" /></svg>
<svg v-else-if="name === 'office'" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path fill-rule="evenodd" d="M7 0h86v100H57.108V88.418H42.892V100H7V0zm9 64h11v15H16V64zm57 0h11v15H73V64zm-19 0h11v15H54V64zm-19 0h11v15H35V64zM16 37h11v15H16V37zm57 0h11v15H73V37zm-19 0h11v15H54V37zm-19 0h11v15H35V37zM16 11h11v15H16V11zm57 0h11v15H73V11zm-19 0h11v15H54V11zm-19 0h11v15H35V11z" /></svg>
<svg v-else-if="name === 'printer'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M4 16H0V6h20v10h-4v4H4v-4zm2-4v6h8v-6H6zM4 0h12v5H4V0zM2 8v2h2V8H2zm4 0v2h2V8H6z" /></svg>
<svg v-else-if="name === 'shopping-cart'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M4 2h16l-3 9H4a1 1 0 1 0 0 2h13v2H4a3 3 0 0 1 0-6h.33L3 5 2 2H0V0h3a1 1 0 0 1 1 1v1zm1 18a2 2 0 1 1 0-4 2 2 0 0 1 0 4zm10 0a2 2 0 1 1 0-4 2 2 0 0 1 0 4z" /></svg>
<svg v-else-if="name === 'store-front'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M18 9.87V20H2V9.87a4.25 4.25 0 0 0 3-.38V14h10V9.5a4.26 4.26 0 0 0 3 .37zM3 0h4l-.67 6.03A3.43 3.43 0 0 1 3 9C1.34 9 .42 7.73.95 6.15L3 0zm5 0h4l.7 6.3c.17 1.5-.91 2.7-2.42 2.7h-.56A2.38 2.38 0 0 1 7.3 6.3L8 0zm5 0h4l2.05 6.15C19.58 7.73 18.65 9 17 9a3.42 3.42 0 0 1-3.33-2.97L13 0z" /></svg>
<svg v-else-if="name === 'trash'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M6 2l2-2h4l2 2h4v2H2V2h4zM3 6h14l-1 14H4L3 6zm5 2v10h1V8H8zm3 0v10h1V8h-1z" /></svg>
<svg v-else-if="name === 'users'" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M7 8a4 4 0 1 1 0-8 4 4 0 0 1 0 8zm0 1c2.15 0 4.2.4 6.1 1.09L12 16h-1.25L10 20H4l-.75-4H2L.9 10.09A17.93 17.93 0 0 1 7 9zm8.31.17c1.32.18 2.59.48 3.8.92L18 16h-1.25L16 20h-3.96l.37-2h1.25l1.65-8.83zM13 0a4 4 0 1 1-1.33 7.76 5.96 5.96 0 0 0 0-7.52C12.1.1 12.53 0 13 0z" /></svg>
</template>
<script>
export default {
props: {
name: String,
},
}
</script>

View File

@ -0,0 +1,91 @@
<template>
<div>
<portal-target name="dropdown" slim />
<div class="flex flex-col">
<div class="min-h-screen flex flex-col" @click="hideDropdownMenus">
<div class="md:flex">
<div class="bg-indigo-darkest md:flex-no-shrink md:w-56 px-6 py-4 flex items-center justify-between md:justify-center">
<inertia-link class="mt-1" href="/">
<logo class="fill-white" width="120" height="28" />
</inertia-link>
<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>
<div slot="dropdown" class="mt-2 px-8 py-4 shadow-lg bg-indigo-darker rounded">
<main-menu />
</div>
</dropdown>
</div>
<div class="bg-white border-b w-full p-4 md:py-0 md:px-12 text-sm md:text-base flex justify-between items-center">
<div class="mt-1 mr-4">{{ page.props.auth.user.account.name }}</div>
<dropdown class="mt-1" placement="bottom-end">
<div class="flex items-center cursor-pointer select-none group">
<div class="text-grey-darkest group-hover:text-indigo-dark focus:text-indigo-dark mr-1 whitespace-no-wrap">
<span>{{ page.props.auth.user.first_name }}</span>
<span class="hidden md:inline">{{ page.props.auth.user.last_name }}</span>
</div>
<icon class="w-5 h-5 group-hover:fill-indigo-dark fill-grey-darkest focus:fill-indigo-dark" name="cheveron-down" />
</div>
<div slot="dropdown" class="mt-2 py-2 shadow-lg bg-white rounded text-sm">
<inertia-link class="block px-6 py-2 hover:bg-indigo hover:text-white" :href="route('users.edit', page.props.auth.user.id)">My Profile</inertia-link>
<inertia-link class="block px-6 py-2 hover:bg-indigo hover:text-white" :href="route('users')">Manage Users</inertia-link>
<inertia-link class="block px-6 py-2 hover:bg-indigo hover:text-white" :href="route('logout')">Logout</inertia-link>
</div>
</dropdown>
</div>
</div>
<div class="flex flex-grow">
<div class="bg-indigo-darker flex-no-shrink w-56 p-12 hidden md:block">
<main-menu />
</div>
<div class="w-full overflow-hidden px-4 py-8 md:p-12">
<slot />
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { InertiaLink } from 'inertia-vue'
import Dropdown from '@/Shared/Dropdown'
import Icon from '@/Shared/Icon'
import Logo from '@/Shared/Logo'
import MainMenu from '@/Shared/MainMenu'
export default {
components: {
InertiaLink,
Dropdown,
Icon,
Logo,
MainMenu,
},
inject: ['page'],
props: {
title: String,
},
data() {
return {
showUserMenu: false,
accounts: null,
}
},
watch: {
title(title) {
this.updatePageTitle(title)
},
},
mounted() {
this.updatePageTitle(this.title)
},
methods: {
updatePageTitle(title) {
document.title = title ? `${title} | ${this.page.props.app.name}` : this.page.props.app.name
},
hideDropdownMenus() {
this.showUserMenu = false
},
},
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<button :disabled="loading" class="flex items-center">
<div v-if="loading" class="spinner mr-2" />
<slot />
</button>
</template>
<script>
export default {
props: {
loading: Boolean,
},
}
</script>

View File

@ -0,0 +1,6 @@
<template>
<svg viewBox="0 0 1185 266" xmlns="http://www.w3.org/2000/svg">
<path d="M77.463 265c-19.497 0-35.318-15.405-35.318-34.39v-22.054C17.987 202.676 0 181.326 0 155.948V55.206C0 25.291 24.946 1 55.668 1h154.664C241.054 1 266 25.29 266 55.206v100.806c0 29.916-24.946 54.206-55.668 54.206H145.67c-2.823 0-5.383 1.407-6.827 3.58-10.7 17.067-24.158 31.897-39.98 43.915-6.236 4.794-13.654 7.287-21.4 7.287zM55.701 27.336c-15.771 0-28.65 12.465-28.65 27.87v100.806c0 15.342 12.813 27.87 28.65 27.87 7.49 0 13.536 5.881 13.536 13.168v33.624c0 4.922 4.272 7.99 8.214 7.99 1.709 0 3.286-.575 4.732-1.662 13.273-10.1 24.576-22.565 33.578-36.947 6.309-10.036 17.743-16.237 29.965-16.237h64.727c15.77 0 28.65-12.464 28.65-27.87V55.206c0-15.341-12.814-27.87-28.65-27.87H55.7z" />
<path d="M395.752 2.4c37.152 0 65.088 27.936 65.088 64.8 0 36.576-27.936 64.8-65.088 64.8h-46.368v72H322.6V2.4h73.152zm0 104.544c22.176 0 38.592-16.992 38.592-39.744 0-23.04-16.416-39.744-38.592-39.744h-46.368v79.488h46.368zM502.6 33.792c-9.504 0-16.992-7.488-16.992-16.704 0-9.216 7.488-16.992 16.992-16.992 9.216 0 16.704 7.776 16.704 16.992 0 9.216-7.488 16.704-16.704 16.704zM489.928 204V60h25.056v144h-25.056zM625 56.256c33.696 0 55.872 22.464 55.872 59.328V204h-25.056v-86.976c0-23.616-13.536-36.864-35.712-36.864-23.04 0-41.76 13.536-41.76 47.52V204h-25.056V60h25.056v20.736C589 63.744 604.84 56.256 625 56.256zM835.24 60h24.768v137.952c0 44.928-36 67.392-73.44 67.392-32.256 0-56.448-12.384-68.256-35.136l21.888-12.384c6.624 13.536 18.72 24.192 46.944 24.192 29.952 0 48.096-16.992 48.096-44.064v-20.448c-11.52 17.568-29.952 28.8-54.144 28.8-40.896 0-73.44-33.12-73.44-75.168 0-41.76 32.544-74.88 73.44-74.88 24.192 0 42.624 10.944 54.144 28.512V60zm-51.264 122.4c29.088 0 51.264-22.176 51.264-51.264 0-28.8-22.176-50.976-51.264-50.976-29.088 0-51.264 22.176-51.264 50.976 0 29.088 22.176 51.264 51.264 51.264zM946.8 205.08c-28.21 0-45.63-20.8-41.08-48.88 4.42-27.17 26.91-46.28 53.56-46.28 19.37 0 31.59 9.36 38.35 22.36l-23.79 12.61c-3.25-5.85-9.1-9.49-16.9-9.49-12.35 0-23.14 9.23-25.35 22.1-2.08 11.83 4.29 22.1 17.16 22.1 8.06 0 13.91-4.03 18.59-10.14l21.58 13.65c-9.36 13.78-24.44 21.97-42.12 21.97zm126.36-59.93c-1.95 11.18-8.58 19.5-18.2 24.44l11.7 33.28h-26l-9.36-28.6h-8.32l-5.07 28.6h-26l16.12-91h36.4c18.33 0 32.24 13.65 28.73 33.28zm-43.42-9.36l-2.99 16.9h10.66c5.07.13 8.84-2.99 9.75-8.32.91-5.33-1.82-8.58-7.02-8.58h-10.4zM1184.05 112l-15.99 91h-26l7.67-43.81-25.48 33.54h-2.34l-14.82-35.23-7.93 45.5h-26l15.99-91h26l13.65 37.31 27.95-37.31h27.3z" />
</svg>
</template>

View File

@ -0,0 +1,49 @@
<template>
<div>
<div class="mb-4">
<inertia-link class="flex items-center group py-3" :href="route('dashboard')">
<icon name="dashboard" class="w-4 h-4 mr-2" :class="isUrl('') ? 'fill-white' : 'fill-indigo-light group-hover:fill-white'" />
<div :class="isUrl('') ? 'text-white' : 'text-indigo-lighter group-hover:text-white'">Dashboard</div>
</inertia-link>
</div>
<div class="mb-4">
<inertia-link class="flex items-center group py-3" :href="route('organizations')">
<icon name="office" class="w-4 h-4 mr-2" :class="isUrl('organizations') ? 'fill-white' : 'fill-indigo-light group-hover:fill-white'" />
<div :class="isUrl('organizations') ? 'text-white' : 'text-indigo-lighter group-hover:text-white'">Organizations</div>
</inertia-link>
</div>
<div class="mb-4">
<inertia-link class="flex items-center group py-3" :href="route('contacts')">
<icon name="users" class="w-4 h-4 mr-2" :class="isUrl('contacts') ? 'fill-white' : 'fill-indigo-light group-hover:fill-white'" />
<div :class="isUrl('contacts') ? 'text-white' : 'text-indigo-lighter group-hover:text-white'">Contacts</div>
</inertia-link>
</div>
<div class="mb-4">
<inertia-link class="flex items-center group py-3" :href="route('reports')">
<icon name="printer" class="w-4 h-4 mr-2" :class="isUrl('reports') ? 'fill-white' : 'fill-indigo-light group-hover:fill-white'" />
<div :class="isUrl('reports') ? 'text-white' : 'text-indigo-lighter group-hover:text-white'">Reports</div>
</inertia-link>
</div>
</div>
</template>
<script>
import { InertiaLink } from 'inertia-vue'
import Icon from '@/Shared/Icon'
export default {
components: {
InertiaLink,
Icon,
},
methods: {
isUrl(...urls) {
if (urls[0] === '') {
return location.pathname.substr(1) === ''
}
return urls.filter(url => location.pathname.substr(1).startsWith(url)).length
},
},
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<div class="mt-6 -mb-1 flex flex-wrap">
<template v-for="(link, key) in links">
<div v-if="link.url === null" :key="key" class="mr-1 mb-1 px-4 py-3 text-sm border rounded text-grey" :class="{ 'ml-auto': link.label === 'Next' }">{{ link.label }}</div>
<inertia-link v-else :key="key" replace class="mr-1 mb-1 px-4 py-3 text-sm border rounded hover:bg-white focus:border-indigo focus:text-indigo" :class="{ 'bg-white': link.active, 'ml-auto': link.label === 'Next' }" :href="link.url">{{ link.label }}</inertia-link>
</template>
</div>
</template>
<script>
import { InertiaLink } from 'inertia-vue'
export default {
components: {
InertiaLink,
},
props: {
links: Array,
},
}
</script>

View File

@ -0,0 +1,34 @@
<template>
<div class="flex items-center">
<div class="flex w-full bg-white shadow rounded">
<dropdown class="px-4 md:px-6 rounded-l border-r flex items-baseline hover:bg-grey-lightest focus:border-white focus:shadow-outline focus:z-10" placement="bottom-start">
<span class="text-grey-darkest hidden md:block">Filter</span>
<svg class="w-2 h-2 fill-grey-darker md:ml-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 961.243 599.998">
<path d="M239.998 239.999L0 0h961.243L721.246 240c-131.999 132-240.28 240-240.624 239.999-.345-.001-108.625-108.001-240.624-240z" />
</svg>
<div slot="dropdown" class="mt-2 px-4 py-6 w-screen shadow-lg bg-white rounded" :style="{ maxWidth: `${maxWidth}px` }">
<slot />
</div>
</dropdown>
<input class="relative w-full px-6 py-3 rounded-r focus:shadow-outline" autocomplete="off" type="text" name="search" placeholder="Search…" :value="value" @input="$emit('input', $event.target.value)">
</div>
<button class="ml-3 text-sm text-grey-dark hover:text-grey-darker focus:text-indigo" type="button" @click="$emit('reset')">Reset</button>
</div>
</template>
<script>
import Dropdown from '@/Shared/Dropdown'
export default {
components: {
Dropdown,
},
props: {
value: String,
maxWidth: {
type: Number,
default: 300,
},
},
}
</script>

View File

@ -0,0 +1,44 @@
<template>
<div>
<label v-if="label" class="form-label" :for="id">{{ label }}:</label>
<select :id="id" ref="input" v-model="selected" v-bind="$attrs" class="form-select" :class="{ error: error }">
<slot />
</select>
<div v-if="error" class="form-error">{{ error }}</div>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
id: {
type: String,
default() {
return `select-input-${this._uid}`
},
},
value: [String, Number],
label: String,
error: String,
},
data() {
return {
selected: this.value,
}
},
watch: {
selected(selected) {
this.$emit('input', selected)
},
},
methods: {
focus() {
this.$refs.input.focus()
},
select() {
this.$refs.input.select()
},
},
}
</script>

View File

@ -0,0 +1,39 @@
<template>
<div>
<label v-if="label" class="form-label" :for="id">{{ label }}:</label>
<input :id="id" ref="input" v-bind="$attrs" class="form-input" :class="{ error: error }" :type="type" :value="value" @input="$emit('input', $event.target.value)">
<div v-if="error" class="form-error">{{ error }}</div>
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
id: {
type: String,
default() {
return `text-input-${this._uid}`
},
},
type: {
type: String,
default: 'text',
},
value: String,
label: String,
error: String,
},
methods: {
focus() {
this.$refs.input.focus()
},
select() {
this.$refs.input.select()
},
setSelectionRange(start, end) {
this.$refs.input.setSelectionRange(start, end)
},
},
}
</script>

View File

@ -0,0 +1,40 @@
<template>
<div>
<label v-if="label" class="form-label" :for="id">{{ label }}:</label>
<textarea :id="id" ref="input" v-bind="$attrs" class="form-textarea" :class="{ error: error }" :value="value" @input="$emit('input', $event.target.value)" />
<div v-if="error" class="form-error">{{ error }}</div>
</div>
</template>
<script>
import autosize from 'autosize'
export default {
inheritAttrs: false,
props: {
id: {
type: String,
default() {
return `textarea-input-${this._uid}`
},
},
value: String,
label: String,
error: String,
autosize: Boolean,
},
mounted() {
if (this.autosize) {
autosize(this.$refs.input)
}
},
methods: {
focus() {
this.$refs.input.focus()
},
select() {
this.$refs.input.select()
},
},
}
</script>

View File

@ -0,0 +1,21 @@
<template>
<div class="p-4 bg-yellow-light rounded border border-yellow-dark flex items-center justify-between">
<div class="flex items-center">
<icon name="trash" class="flex-no-shrink w-4 h-4 fill-yellow-darker mr-2" />
<div class="text-yellow-darker">
<slot />
</div>
</div>
<button class="text-yellow-darker hover:underline" tabindex="-1" type="button" @click="$emit('restore')">Restore</button>
</div>
</template>
<script>
import Icon from '@/Shared/Icon'
export default {
components: {
Icon,
},
}
</script>

31
resources/js/Utils/Errors.js vendored Normal file
View File

@ -0,0 +1,31 @@
class Errors {
constructor(errors = {}) {
this.record(errors)
}
record(errors = {}) {
this.errors = errors
}
all() {
return this.errors
}
any() {
return Object.keys(this.errors).length > 0
}
has(key) {
return key in this.errors
}
first(field) {
return this.get(field)[0]
}
get(field) {
return this.errors[field] || []
}
}
export default Errors

44
resources/js/Utils/Form.js vendored Normal file
View File

@ -0,0 +1,44 @@
import axios from 'axios'
import Errors from '@/Utils/Errors'
class Form {
constructor(fields = {}) {
this.fields = fields
this.sending = false
this.errors = new Errors()
this.http = axios.create({
headers: { 'X-Requested-With': 'XMLHttpRequest' },
})
}
delete({ url, then }) {
this.request(this.http.delete(url), then)
}
post({ url, data = this.fields, then }) {
this.request(this.http.post(url, data), then)
}
put({ url, data = this.fields, then }) {
this.request(this.http.put(url, data), then)
}
request(request, then) {
this.sending = true
request.then(response => {
this.sending = false
then(response.data)
}).catch(error => {
this.sending = false
if (error.response && error.response.status === 422) {
this.errors.record(error.response.data.errors)
} else {
return Promise.reject(error)
}
})
}
}
export default Form

48
resources/js/app.js vendored
View File

@ -1,33 +1,21 @@
import Inertia from 'inertia-vue'
import PortalVue from 'portal-vue'
import Vue from 'vue'
/** Vue.config.productionTip = false
* First we will load all of this project's JavaScript dependencies which Vue.mixin({ methods: { route: window.route } })
* includes Vue and other libraries. It is a great starting point when Vue.use(PortalVue)
* building robust, powerful web applications using Vue and Laravel.
*/
require('./bootstrap'); let app = document.getElementById('app')
window.Vue = require('vue'); new Vue({
render: h => h(Inertia, {
/** props: {
* The following block of code may be used to automatically register your component: app.dataset.component,
* Vue components. It will recursively scan this directory for the Vue props: JSON.parse(app.dataset.props),
* components and automatically register them with their "basename". resolveComponent: (component) => {
* return import(`@/Pages/${component}`).then(module => module.default)
* Eg. ./components/ExampleComponent.vue -> <example-component></example-component> },
*/ },
}),
// const files = require.context('./', true, /\.vue$/i); }).$mount(app)
// files.keys().map(key => Vue.component(key.split('/').pop().split('.')[0], files(key).default));
Vue.component('example-component', require('./components/ExampleComponent.vue').default);
/**
* Next, we will create a fresh Vue application instance and attach it to
* the page. Then, you may begin adding components to this application
* or customize the JavaScript scaffolding to fit your unique needs.
*/
const app = new Vue({
el: '#app'
});

View File

@ -1,56 +0,0 @@
window._ = require('lodash');
/**
* We'll load jQuery and the Bootstrap jQuery plugin which provides support
* for JavaScript based Bootstrap features such as modals and tabs. This
* code may be modified to fit the specific needs of your application.
*/
try {
window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap');
} catch (e) {}
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Next we will register the CSRF Token as a common header with Axios so that
* all outgoing HTTP requests automatically have it attached. This is just
* a simple convenience so we don't have to attach every token manually.
*/
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
} else {
console.error('CSRF token not found: https://laravel.com/docs/csrf#csrf-x-csrf-token');
}
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo'
// window.Pusher = require('pusher-js');
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: process.env.MIX_PUSHER_APP_KEY,
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
// encrypted: true
// });

View File

@ -1,23 +0,0 @@
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Example Component</div>
<div class="card-body">
I'm an example component.
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
console.log('Component mounted.')
}
}
</script>

View File

@ -1,20 +0,0 @@
// Body
$body-bg: #f8fafc;
// Typography
$font-family-sans-serif: "Nunito", sans-serif;
$font-size-base: 0.9rem;
$line-height-base: 1.6;
// Colors
$blue: #3490dc;
$indigo: #6574cd;
$purple: #9561e2;
$pink: #f66D9b;
$red: #e3342f;
$orange: #f6993f;
$yellow: #ffed4a;
$green: #38c172;
$teal: #4dc0b5;
$cyan: #6cb2eb;

View File

@ -1,14 +0,0 @@
// Fonts
@import url('https://fonts.googleapis.com/css?family=Nunito');
// Variables
@import 'variables';
// Bootstrap
@import '~bootstrap/scss/bootstrap';
.navbar-laravel {
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04);
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html class="h-full bg-grey-lighter">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link href="{{ mix('/css/app.css') }}" rel="stylesheet">
<script src="{{ mix('/js/app.js') }}" defer></script>
@routes
</head>
<body class="font-sans leading-none text-grey-darkest antialiased">
<div id="app" data-component="{{ $component }}" data-props="{{ json_encode($props) }}" />
</body>
</html>

View File

@ -1,99 +0,0 @@
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
<!-- Fonts -->
<link href="https://fonts.googleapis.com/css?family=Nunito:200,600" rel="stylesheet">
<!-- Styles -->
<style>
html, body {
background-color: #fff;
color: #636b6f;
font-family: 'Nunito', sans-serif;
font-weight: 200;
height: 100vh;
margin: 0;
}
.full-height {
height: 100vh;
}
.flex-center {
align-items: center;
display: flex;
justify-content: center;
}
.position-ref {
position: relative;
}
.top-right {
position: absolute;
right: 10px;
top: 18px;
}
.content {
text-align: center;
}
.title {
font-size: 84px;
}
.links > a {
color: #636b6f;
padding: 0 25px;
font-size: 13px;
font-weight: 600;
letter-spacing: .1rem;
text-decoration: none;
text-transform: uppercase;
}
.m-b-md {
margin-bottom: 30px;
}
</style>
</head>
<body>
<div class="flex-center position-ref full-height">
@if (Route::has('login'))
<div class="top-right links">
@auth
<a href="{{ url('/home') }}">Home</a>
@else
<a href="{{ route('login') }}">Login</a>
@if (Route::has('register'))
<a href="{{ route('register') }}">Register</a>
@endif
@endauth
</div>
@endif
<div class="content">
<div class="title m-b-md">
Laravel
</div>
<div class="links">
<a href="https://laravel.com/docs">Docs</a>
<a href="https://laracasts.com">Laracasts</a>
<a href="https://laravel-news.com">News</a>
<a href="https://blog.laravel.com">Blog</a>
<a href="https://nova.laravel.com">Nova</a>
<a href="https://forge.laravel.com">Forge</a>
<a href="https://github.com/laravel/laravel">GitHub</a>
</div>
</div>
</div>
</body>
</html>

View File

@ -11,6 +11,54 @@
| |
*/ */
Route::get('/', function () { // Auth
return view('welcome'); Route::get('login')->name('login')->uses('Auth\LoginController@showLoginForm');
Route::post('login')->name('login.attempt')->uses('Auth\LoginController@login');
Route::get('logout')->name('logout')->uses('Auth\LoginController@logout');
// Dashboard
Route::get('/')->name('dashboard')->uses('DashboardController@index')->middleware('remember', 'auth');
// Accounts
Route::get('accounts')->name('accounts')->uses('AccountsController@index')->middleware('remember', 'auth');
Route::get('accounts/create')->name('accounts.create')->uses('AccountsController@create')->middleware('auth');
Route::post('accounts')->name('accounts.store')->uses('AccountsController@store')->middleware('auth');
Route::get('accounts/{account}/edit')->name('accounts.edit')->uses('AccountsController@edit')->middleware('auth');
Route::put('accounts/{account}')->name('accounts.update')->uses('AccountsController@update')->middleware('auth');
Route::delete('accounts/{account}')->name('accounts.destroy')->uses('AccountsController@destroy')->middleware('auth');
Route::put('accounts/{account}/restore')->name('accounts.restore')->uses('AccountsController@restore')->middleware('auth');
// Users
Route::get('users')->name('users')->uses('UsersController@index')->middleware('remember', 'auth');
Route::get('users/create')->name('users.create')->uses('UsersController@create')->middleware('auth');
Route::post('users')->name('users.store')->uses('UsersController@store')->middleware('auth');
Route::get('users/{user}/edit')->name('users.edit')->uses('UsersController@edit')->middleware('auth');
Route::put('users/{user}')->name('users.update')->uses('UsersController@update')->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');
// Organizations
Route::get('organizations')->name('organizations')->uses('OrganizationsController@index')->middleware('remember', 'auth');
Route::get('organizations/create')->name('organizations.create')->uses('OrganizationsController@create')->middleware('auth');
Route::post('organizations')->name('organizations.store')->uses('OrganizationsController@store')->middleware('auth');
Route::get('organizations/{organization}/edit')->name('organizations.edit')->uses('OrganizationsController@edit')->middleware('auth');
Route::put('organizations/{organization}')->name('organizations.update')->uses('OrganizationsController@update')->middleware('auth');
Route::delete('organizations/{organization}')->name('organizations.destroy')->uses('OrganizationsController@destroy')->middleware('auth');
Route::put('organizations/{organization}/restore')->name('organizations.restore')->uses('OrganizationsController@restore')->middleware('auth');
// Contacts
Route::get('contacts')->name('contacts')->uses('ContactsController@index')->middleware('remember', 'auth');
Route::get('contacts/create')->name('contacts.create')->uses('ContactsController@create')->middleware('auth');
Route::post('contacts')->name('contacts.store')->uses('ContactsController@store')->middleware('auth');
Route::get('contacts/{contact}/edit')->name('contacts.edit')->uses('ContactsController@edit')->middleware('auth');
Route::put('contacts/{contact}')->name('contacts.update')->uses('ContactsController@update')->middleware('auth');
Route::delete('contacts/{contact}')->name('contacts.destroy')->uses('ContactsController@destroy')->middleware('auth');
Route::put('contacts/{contact}/restore')->name('contacts.restore')->uses('ContactsController@restore')->middleware('auth');
// Reports
Route::get('reports')->name('reports')->uses('ReportsController@index')->middleware('auth');
// 500 error
Route::get('500', function () {
echo $fail;
}); });

2
storage/debugbar/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

924
tailwind.js vendored Normal file
View File

@ -0,0 +1,924 @@
/*
Tailwind - The Utility-First CSS Framework
A project by Adam Wathan (@adamwathan), Jonathan Reinink (@reinink),
David Hemphill (@davidhemphill) and Steve Schoger (@steveschoger).
Welcome to the Tailwind config file. This is where you can customize
Tailwind specifically for your project. Don't be intimidated by the
length of this file. It's really just a big JavaScript object and
we've done our very best to explain each section.
View the full documentation at https://tailwindcss.com.
|-------------------------------------------------------------------------------
| The default config
|-------------------------------------------------------------------------------
|
| This variable contains the default Tailwind config. You don't have
| to use it, but it can sometimes be helpful to have available. For
| example, you may choose to merge your custom configuration
| values with some of the Tailwind defaults.
|
*/
let defaultConfig = require('tailwindcss/defaultConfig')()
/*
|-------------------------------------------------------------------------------
| Colors https://tailwindcss.com/docs/colors
|-------------------------------------------------------------------------------
|
| Here you can specify the colors used in your project. To get you started,
| we've provided a generous palette of great looking colors that are perfect
| for prototyping, but don't hesitate to change them for your project. You
| own these colors, nothing will break if you change everything about them.
|
| We've used literal color names ("red", "blue", etc.) for the default
| palette, but if you'd rather use functional names like "primary" and
| "secondary", or even a numeric scale like "100" and "200", go for it.
|
*/
let colors = {
'transparent': 'transparent',
'black': '#22292f',
'grey-darkest': '#3d4852',
'grey-darker': '#606f7b',
'grey-dark': '#8795a1',
'grey': '#b8c2cc',
'grey-light': '#dae1e7',
'grey-lighter': '#f1f5f8',
'grey-lightest': '#f8fafc',
'white': '#ffffff',
'red-darkest': '#3b0d0c',
'red-darker': '#621b18',
'red-dark': '#cc1f1a',
'red': '#e3342f',
'red-light': '#ef5753',
'red-lighter': '#f9acaa',
'red-lightest': '#fcebea',
'orange-darkest': '#462a16',
'orange-darker': '#613b1f',
'orange-dark': '#de751f',
'orange': '#f6993f',
'orange-light': '#faad63',
'orange-lighter': '#fcd9b6',
'orange-lightest': '#fff5eb',
'yellow-darkest': '#453411',
'yellow-darker': '#684f1d',
'yellow-dark': '#f2d024',
'yellow': '#ffed4a',
'yellow-light': '#fff382',
'yellow-lighter': '#fff9c2',
'yellow-lightest': '#fcfbeb',
'green-darkest': '#0f2f21',
'green-darker': '#1a4731',
'green-dark': '#1f9d55',
'green': '#38c172',
'green-light': '#51d88a',
'green-lighter': '#a2f5bf',
'green-lightest': '#e3fcec',
'teal-darkest': '#0d3331',
'teal-darker': '#20504f',
'teal-dark': '#38a89d',
'teal': '#4dc0b5',
'teal-light': '#64d5ca',
'teal-lighter': '#a0f0ed',
'teal-lightest': '#e8fffe',
'blue-darkest': '#12283a',
'blue-darker': '#1c3d5a',
'blue-dark': '#2779bd',
'blue': '#3490dc',
'blue-light': '#6cb2eb',
'blue-lighter': '#bcdefa',
'blue-lightest': '#eff8ff',
'indigo-darkest': '#191e38',
'indigo-darker': '#2f365f',
'indigo-dark': '#5661b3',
'indigo': '#6574cd',
'indigo-light': '#7886d7',
'indigo-lighter': '#b2b7ff',
'indigo-lightest': '#e6e8ff',
'purple-darkest': '#21183c',
'purple-darker': '#382b5f',
'purple-dark': '#794acf',
'purple': '#9561e2',
'purple-light': '#a779e9',
'purple-lighter': '#d6bbfc',
'purple-lightest': '#f3ebff',
'pink-darkest': '#451225',
'pink-darker': '#6f213f',
'pink-dark': '#eb5286',
'pink': '#f66d9b',
'pink-light': '#fa7ea8',
'pink-lighter': '#ffbbca',
'pink-lightest': '#ffebef',
}
module.exports = {
/*
|-----------------------------------------------------------------------------
| Colors https://tailwindcss.com/docs/colors
|-----------------------------------------------------------------------------
|
| The color palette defined above is also assigned to the "colors" key of
| your Tailwind config. This makes it easy to access them in your CSS
| using Tailwind's config helper. For example:
|
| .error { color: config('colors.red') }
|
*/
colors: colors,
/*
|-----------------------------------------------------------------------------
| Screens https://tailwindcss.com/docs/responsive-design
|-----------------------------------------------------------------------------
|
| Screens in Tailwind are translated to CSS media queries. They define the
| responsive breakpoints for your project. By default Tailwind takes a
| "mobile first" approach, where each screen size represents a minimum
| viewport width. Feel free to have as few or as many screens as you
| want, naming them in whatever way you'd prefer for your project.
|
| Tailwind also allows for more complex screen definitions, which can be
| useful in certain situations. Be sure to see the full responsive
| documentation for a complete list of options.
|
| Class name: .{screen}:{utility}
|
*/
screens: {
'sm': '576px',
'md': '768px',
'lg': '992px',
'xl': '1200px',
},
/*
|-----------------------------------------------------------------------------
| Fonts https://tailwindcss.com/docs/fonts
|-----------------------------------------------------------------------------
|
| Here is where you define your project's font stack, or font families.
| Keep in mind that Tailwind doesn't actually load any fonts for you.
| If you're using custom fonts you'll need to import them prior to
| defining them here.
|
| By default we provide a native font stack that works remarkably well on
| any device or OS you're using, since it just uses the default fonts
| provided by the platform.
|
| Class name: .font-{name}
| CSS property: font-family
|
*/
fonts: {
'sans': [
'Cerebri Sans',
'system-ui',
'BlinkMacSystemFont',
'-apple-system',
'Segoe UI',
'Roboto',
'Oxygen',
'Ubuntu',
'Cantarell',
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
'sans-serif',
],
'serif': [
'Constantia',
'Lucida Bright',
'Lucidabright',
'Lucida Serif',
'Lucida',
'DejaVu Serif',
'Bitstream Vera Serif',
'Liberation Serif',
'Georgia',
'serif',
],
'mono': [
'Menlo',
'Monaco',
'Consolas',
'Liberation Mono',
'Courier New',
'monospace',
],
},
/*
|-----------------------------------------------------------------------------
| Text sizes https://tailwindcss.com/docs/text-sizing
|-----------------------------------------------------------------------------
|
| Here is where you define your text sizes. Name these in whatever way
| makes the most sense to you. We use size names by default, but
| you're welcome to use a numeric scale or even something else
| entirely.
|
| By default Tailwind uses the "rem" unit type for most measurements.
| This allows you to set a root font size which all other sizes are
| then based on. That said, you are free to use whatever units you
| prefer, be it rems, ems, pixels or other.
|
| Class name: .text-{size}
| CSS property: font-size
|
*/
textSizes: {
'xs': '.75rem', // 12px
'sm': '.875rem', // 14px
'md': '1rem', // 16px
'lg': '1.125rem', // 18px
'xl': '1.25rem', // 20px
'2xl': '1.5rem', // 24px
'3xl': '1.875rem', // 30px
'4xl': '2.25rem', // 36px
'5xl': '3rem', // 48px
},
/*
|-----------------------------------------------------------------------------
| Font weights https://tailwindcss.com/docs/font-weight
|-----------------------------------------------------------------------------
|
| Here is where you define your font weights. We've provided a list of
| common font weight names with their respective numeric scale values
| to get you started. It's unlikely that your project will require
| all of these, so we recommend removing those you don't need.
|
| Class name: .font-{weight}
| CSS property: font-weight
|
*/
fontWeights: {
'hairline': 100,
'thin': 200,
'light': 300,
'normal': 400,
'medium': 500,
'semibold': 600,
'bold': 700,
'extrabold': 800,
'black': 900,
},
/*
|-----------------------------------------------------------------------------
| Leading (line height) https://tailwindcss.com/docs/line-height
|-----------------------------------------------------------------------------
|
| Here is where you define your line height values, or as we call
| them in Tailwind, leadings.
|
| Class name: .leading-{size}
| CSS property: line-height
|
*/
leading: {
'none': 1,
'tight': 1.25,
'normal': 1.5,
'loose': 2,
},
/*
|-----------------------------------------------------------------------------
| Tracking (letter spacing) https://tailwindcss.com/docs/letter-spacing
|-----------------------------------------------------------------------------
|
| Here is where you define your letter spacing values, or as we call
| them in Tailwind, tracking.
|
| Class name: .tracking-{size}
| CSS property: letter-spacing
|
*/
tracking: {
'tight': '-0.05em',
'normal': '0',
'wide': '0.05em',
},
/*
|-----------------------------------------------------------------------------
| Text colors https://tailwindcss.com/docs/text-color
|-----------------------------------------------------------------------------
|
| Here is where you define your text colors. By default these use the
| color palette we defined above, however you're welcome to set these
| independently if that makes sense for your project.
|
| Class name: .text-{color}
| CSS property: color
|
*/
textColors: colors,
/*
|-----------------------------------------------------------------------------
| Background colors https://tailwindcss.com/docs/background-color
|-----------------------------------------------------------------------------
|
| Here is where you define your background colors. By default these use
| the color palette we defined above, however you're welcome to set
| these independently if that makes sense for your project.
|
| Class name: .bg-{color}
| CSS property: background-color
|
*/
backgroundColors: colors,
/*
|-----------------------------------------------------------------------------
| Background sizes https://tailwindcss.com/docs/background-size
|-----------------------------------------------------------------------------
|
| Here is where you define your background sizes. We provide some common
| values that are useful in most projects, but feel free to add other sizes
| that are specific to your project here as well.
|
| Class name: .bg-{size}
| CSS property: background-size
|
*/
backgroundSize: {
'auto': 'auto',
'cover': 'cover',
'contain': 'contain',
},
/*
|-----------------------------------------------------------------------------
| Border widths https://tailwindcss.com/docs/border-width
|-----------------------------------------------------------------------------
|
| Here is where you define your border widths. Take note that border
| widths require a special "default" value set as well. This is the
| width that will be used when you do not specify a border width.
|
| Class name: .border{-side?}{-width?}
| CSS property: border-width
|
*/
borderWidths: {
default: '1px',
'0': '0',
'2': '2px',
'4': '4px',
'8': '8px',
},
/*
|-----------------------------------------------------------------------------
| Border colors https://tailwindcss.com/docs/border-color
|-----------------------------------------------------------------------------
|
| Here is where you define your border colors. By default these use the
| color palette we defined above, however you're welcome to set these
| independently if that makes sense for your project.
|
| Take note that border colors require a special "default" value set
| as well. This is the color that will be used when you do not
| specify a border color.
|
| Class name: .border-{color}
| CSS property: border-color
|
*/
borderColors: global.Object.assign({ default: colors['grey-light'] }, colors),
/*
|-----------------------------------------------------------------------------
| Border radius https://tailwindcss.com/docs/border-radius
|-----------------------------------------------------------------------------
|
| Here is where you define your border radius values. If a `default` radius
| is provided, it will be made available as the non-suffixed `.rounded`
| utility.
|
| If your scale includes a `0` value to reset already rounded corners, it's
| a good idea to put it first so other values are able to override it.
|
| Class name: .rounded{-side?}{-size?}
| CSS property: border-radius
|
*/
borderRadius: {
'none': '0',
'sm': '.125rem',
default: '.25rem',
'lg': '.5rem',
'full': '9999px',
},
/*
|-----------------------------------------------------------------------------
| Width https://tailwindcss.com/docs/width
|-----------------------------------------------------------------------------
|
| Here is where you define your width utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default
| we provide a sensible rem based numeric scale, a percentage
| based fraction scale, plus some other common use-cases. You
| can, of course, modify these values as needed.
|
|
| It's also worth mentioning that Tailwind automatically escapes
| invalid CSS class name characters, which allows you to have
| awesome classes like .w-2/3.
|
| Class name: .w-{size}
| CSS property: width
|
*/
width: {
'auto': 'auto',
'px': '1px',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'24': '6rem',
'32': '8rem',
'48': '12rem',
'56': '14rem',
'64': '16rem',
'1/2': '50%',
'1/3': '33.33333%',
'2/3': '66.66667%',
'1/4': '25%',
'3/4': '75%',
'1/5': '20%',
'2/5': '40%',
'3/5': '60%',
'4/5': '80%',
'1/6': '16.66667%',
'5/6': '83.33333%',
'full': '100%',
'screen': '100vw',
},
/*
|-----------------------------------------------------------------------------
| Height https://tailwindcss.com/docs/height
|-----------------------------------------------------------------------------
|
| Here is where you define your height utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default
| we provide a sensible rem based numeric scale plus some other
| common use-cases. You can, of course, modify these values as
| needed.
|
| Class name: .h-{size}
| CSS property: height
|
*/
height: {
'auto': 'auto',
'px': '1px',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'24': '6rem',
'32': '8rem',
'48': '12rem',
'64': '16rem',
'full': '100%',
'screen': '100vh',
},
/*
|-----------------------------------------------------------------------------
| Minimum width https://tailwindcss.com/docs/min-width
|-----------------------------------------------------------------------------
|
| Here is where you define your minimum width utility sizes. These can
| be percentage based, pixels, rems, or any other units. We provide a
| couple common use-cases by default. You can, of course, modify
| these values as needed.
|
| Class name: .min-w-{size}
| CSS property: min-width
|
*/
minWidth: {
'0': '0',
'full': '100%',
},
/*
|-----------------------------------------------------------------------------
| Minimum height https://tailwindcss.com/docs/min-height
|-----------------------------------------------------------------------------
|
| Here is where you define your minimum height utility sizes. These can
| be percentage based, pixels, rems, or any other units. We provide a
| few common use-cases by default. You can, of course, modify these
| values as needed.
|
| Class name: .min-h-{size}
| CSS property: min-height
|
*/
minHeight: {
'0': '0',
'full': '100%',
'screen': '100vh',
},
/*
|-----------------------------------------------------------------------------
| Maximum width https://tailwindcss.com/docs/max-width
|-----------------------------------------------------------------------------
|
| Here is where you define your maximum width utility sizes. These can
| be percentage based, pixels, rems, or any other units. By default
| we provide a sensible rem based scale and a "full width" size,
| which is basically a reset utility. You can, of course,
| modify these values as needed.
|
| Class name: .max-w-{size}
| CSS property: max-width
|
*/
maxWidth: {
'xs': '20rem',
'sm': '30rem',
'md': '40rem',
'lg': '50rem',
'xl': '60rem',
'2xl': '70rem',
'3xl': '80rem',
'4xl': '90rem',
'5xl': '100rem',
'full': '100%',
},
/*
|-----------------------------------------------------------------------------
| Maximum height https://tailwindcss.com/docs/max-height
|-----------------------------------------------------------------------------
|
| Here is where you define your maximum height utility sizes. These can
| be percentage based, pixels, rems, or any other units. We provide a
| couple common use-cases by default. You can, of course, modify
| these values as needed.
|
| Class name: .max-h-{size}
| CSS property: max-height
|
*/
maxHeight: {
'full': '100%',
'screen': '100vh',
},
/*
|-----------------------------------------------------------------------------
| Padding https://tailwindcss.com/docs/padding
|-----------------------------------------------------------------------------
|
| Here is where you define your padding utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default we
| provide a sensible rem based numeric scale plus a couple other
| common use-cases like "1px". You can, of course, modify these
| values as needed.
|
| Class name: .p{side?}-{size}
| CSS property: padding
|
*/
padding: {
'px': '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
},
/*
|-----------------------------------------------------------------------------
| Margin https://tailwindcss.com/docs/margin
|-----------------------------------------------------------------------------
|
| Here is where you define your margin utility sizes. These can be
| percentage based, pixels, rems, or any other units. By default we
| provide a sensible rem based numeric scale plus a couple other
| common use-cases like "1px". You can, of course, modify these
| values as needed.
|
| Class name: .m{side?}-{size}
| CSS property: margin
|
*/
margin: {
'auto': 'auto',
'px': '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
},
/*
|-----------------------------------------------------------------------------
| Negative margin https://tailwindcss.com/docs/negative-margin
|-----------------------------------------------------------------------------
|
| Here is where you define your negative margin utility sizes. These can
| be percentage based, pixels, rems, or any other units. By default we
| provide matching values to the padding scale since these utilities
| generally get used together. You can, of course, modify these
| values as needed.
|
| Class name: .-m{side?}-{size}
| CSS property: margin
|
*/
negativeMargin: {
'px': '1px',
'0': '0',
'1': '0.25rem',
'2': '0.5rem',
'3': '0.75rem',
'4': '1rem',
'5': '1.25rem',
'6': '1.5rem',
'8': '2rem',
'10': '2.5rem',
'12': '3rem',
'16': '4rem',
'20': '5rem',
'24': '6rem',
'32': '8rem',
},
/*
|-----------------------------------------------------------------------------
| Shadows https://tailwindcss.com/docs/shadows
|-----------------------------------------------------------------------------
|
| Here is where you define your shadow utilities. As you can see from
| the defaults we provide, it's possible to apply multiple shadows
| per utility using comma separation.
|
| If a `default` shadow is provided, it will be made available as the non-
| suffixed `.shadow` utility.
|
| Class name: .shadow-{size?}
| CSS property: box-shadow
|
*/
shadows: {
default: '0 2px 4px 0 rgba(0,0,0,0.10)',
'md': '0 4px 8px 0 rgba(0,0,0,0.12), 0 2px 4px 0 rgba(0,0,0,0.08)',
'lg': '0 15px 30px 0 rgba(0,0,0,0.11), 0 5px 15px 0 rgba(0,0,0,0.08)',
'inner': 'inset 0 2px 4px 0 rgba(0,0,0,0.06)',
'outline': '0 0 0 2px #6574cd',
'none': 'none',
},
/*
|-----------------------------------------------------------------------------
| Z-index https://tailwindcss.com/docs/z-index
|-----------------------------------------------------------------------------
|
| Here is where you define your z-index utility values. By default we
| provide a sensible numeric scale. You can, of course, modify these
| values as needed.
|
| Class name: .z-{index}
| CSS property: z-index
|
*/
zIndex: {
'auto': 'auto',
'0': 0,
'10': 10,
'20': 20,
'30': 30,
'40': 40,
'50': 50,
},
/*
|-----------------------------------------------------------------------------
| Opacity https://tailwindcss.com/docs/opacity
|-----------------------------------------------------------------------------
|
| Here is where you define your opacity utility values. By default we
| provide a sensible numeric scale. You can, of course, modify these
| values as needed.
|
| Class name: .opacity-{name}
| CSS property: opacity
|
*/
opacity: {
'0': '0',
'25': '.25',
'50': '.5',
'75': '.75',
'100': '1',
},
/*
|-----------------------------------------------------------------------------
| SVG fill https://tailwindcss.com/docs/svg
|-----------------------------------------------------------------------------
|
| Here is where you define your SVG fill colors. By default we just provide
| `fill-current` which sets the fill to the current text color. This lets you
| specify a fill color using existing text color utilities and helps keep the
| generated CSS file size down.
|
| Class name: .fill-{name}
| CSS property: fill
|
*/
svgFill: global.Object.assign({ 'current': 'currentColor' }, colors),
/*
|-----------------------------------------------------------------------------
| SVG stroke https://tailwindcss.com/docs/svg
|-----------------------------------------------------------------------------
|
| Here is where you define your SVG stroke colors. By default we just provide
| `stroke-current` which sets the stroke to the current text color. This lets
| you specify a stroke color using existing text color utilities and helps
| keep the generated CSS file size down.
|
| Class name: .stroke-{name}
| CSS property: stroke
|
*/
svgStroke: {
'current': 'currentColor',
},
/*
|-----------------------------------------------------------------------------
| Modules https://tailwindcss.com/docs/configuration#modules
|-----------------------------------------------------------------------------
|
| Here is where you control which modules are generated and what variants are
| generated for each of those modules.
|
| Currently supported variants:
| - responsive
| - hover
| - focus
| - focus-within
| - active
| - group-hover
|
| To disable a module completely, use `false` instead of an array.
|
*/
modules: 'all',
/*
|-----------------------------------------------------------------------------
| Plugins https://tailwindcss.com/docs/plugins
|-----------------------------------------------------------------------------
|
| Here is where you can register any plugins you'd like to use in your
| project. Tailwind's built-in `container` plugin is enabled by default to
| give you a Bootstrap-style responsive container component out of the box.
|
| Be sure to view the complete plugin documentation to learn more about how
| the plugin system works.
|
*/
plugins: [],
/*
|-----------------------------------------------------------------------------
| Advanced Options https://tailwindcss.com/docs/configuration#options
|-----------------------------------------------------------------------------
|
| Here is where you can tweak advanced configuration options. We recommend
| leaving these options alone unless you absolutely need to change them.
|
*/
options: {
prefix: '',
important: false,
separator: ':',
},
}

22
webpack.mix.js vendored
View File

@ -1,4 +1,8 @@
const mix = require('laravel-mix'); const cssImport = require('postcss-import')
const cssNesting = require('postcss-nesting')
const mix = require('laravel-mix')
const path = require('path')
const tailwindcss = require('tailwindcss')
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -12,4 +16,18 @@ const mix = require('laravel-mix');
*/ */
mix.js('resources/js/app.js', 'public/js') mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css'); .postCss('resources/css/app.css', 'public/css', [
cssImport(),
cssNesting(),
tailwindcss('tailwind.js'),
])
.webpackConfig({
output: { chunkFilename: 'js/[name].[contenthash].js' },
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.js',
'@': path.resolve('resources/js'),
},
},
})
.version()