Start on user profile / permissions.

This commit is contained in:
Ryan Scherler
2020-11-30 14:55:06 -08:00
parent 76c2bfcf8d
commit 075c0cf03e
30 changed files with 268 additions and 42 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ class Page extends Resource
{
public $group = 'Content'; // ability to group resources for organization
public $label = 'My Pages'; // defaults to formatted & pluralized classname
public $icon = 'heroicon-o-database';
public $icon = 'heroicon-o-document-text';
public $sort = 0;
public function actions()
+3
View File
@@ -17,6 +17,7 @@ return [
'features' => [
Features::registration(),
// Features::profile(),
// Features::permissions(),
Features::resources(),
],
@@ -102,6 +103,7 @@ return [
'components' => [
'nav' => Filament\View\Components\Nav::class,
'avatar' => Filament\View\Components\Avatar::class,
],
/*
@@ -122,6 +124,7 @@ return [
'logout' => Filament\Http\Livewire\Auth\Logout::class,
'dashboard' => Filament\Http\Livewire\Dashboard::class,
'profile' => Filament\Http\Livewire\Profile::class,
'account' => Filament\Http\Livewire\Account::class,
],
/*
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,6 +1,6 @@
{
"/js/filament.js": "/js/filament.js?id=9995e728ce8648b81f5d",
"/css/filament.css": "/css/filament.css?id=cf45ade5f9467ef81461",
"/css/filament.css": "/css/filament.css?id=4de496e09c2e4d91eaf1",
"/js/filament.js.map": "/js/filament.js.map?id=1d3615b89069bfd79261",
"/css/filament.css.map": "/css/filament.css.map?id=0a07cf2d478cf17a4122"
"/css/filament.css.map": "/css/filament.css.map?id=fe942c51a9660e9597a3"
}
+2 -2
View File
@@ -1,8 +1,8 @@
.btn {
@apply font-medium py-2 px-4 bg-gray-100 text-gray-600 rounded transition duration-200 shadow-sm inline-block relative focus:ring focus:ring-indigo-200 focus:ring-opacity-50 hover:bg-gray-200;
@apply text-sm font-medium py-2 px-4 bg-gray-100 text-gray-600 rounded transition duration-200 shadow-sm inline-block relative focus:ring focus:ring-indigo-200 focus:ring-opacity-50 hover:bg-gray-200;
&-primary {
@apply bg-gray-700 text-white hover:bg-gray-800;
@apply bg-blue-700 text-white hover:bg-blue-800;
}
&[disabled] {
+3
View File
@@ -0,0 +1,3 @@
.card {
@apply p-4 md:p-6 bg-white shadow rounded overflow-hidden;
}
+1
View File
@@ -9,6 +9,7 @@
@layer components {
@import './components/buttons.css';
@import './components/lists.css';
@import './components/cards.css';
}
@layer utilities {
@@ -0,0 +1,8 @@
<img {{ $attributes->merge([
'src' => $src(),
'srcset' => $srcSet(),
'alt' => $user->name,
'width' => $size,
'height' => $size,
'loading' => 'lazy'
]) }} />
@@ -0,0 +1,7 @@
@props([
'cols' => 2,
])
<div {{ $attributes->merge(['class' => 'grid grid-cols-1 gap-2 lg:gap-6 lg:grid-cols-'.$cols]) }}>
{{ $slot }}
</div>
@@ -0,0 +1,7 @@
@props([
'submit',
])
<form wire:submit.prevent="{{ $submit }}" {{ $attributes->merge(['class' => 'space-y-4']) }}>
{{ $slot }}
</form>
+1 -1
View File
@@ -1,3 +1,3 @@
<span {{ $attributes->merge(['class' => 'text-sm font-normal leading-tight text-gray-700']) }}>
<span {{ $attributes->merge(['class' => 'text-xs font-normal leading-tight text-gray-500']) }}>
{{ $slot }}
</span>
+1 -1
View File
@@ -1,3 +1,3 @@
<span {{ $attributes->merge(['class' => 'font-normal font-mono text-xs leading-tight text-gray-700']) }}>
<span {{ $attributes->merge(['class' => 'font-normal font-mono text-xs leading-tight text-gray-500']) }}>
{{ $slot }}
</span>
@@ -0,0 +1,8 @@
<div {{ $attributes->merge(['class' => 'p-4 md:p-6']) }}
x-show="tab === '{{ $id }}'"
tabindex="0"
role="tabpanel"
id="{{ $id }}-tab"
aria-labelledby="{{ $id }}">
{{ $slot }}
</div>
+9
View File
@@ -0,0 +1,9 @@
<button role="tab"
class="text-sm leading-tight font-semibold p-3 md:px-6 -mb-px border-r border-gray-200"
:class="{ 'bg-white': tab === '{{ $id }}' }"
:aria-selected="tab === '{{ $id }}'"
aria-controls="{{ $id }}-tab" id="{{ $id }}"
:tabindex="tab === '{{ $id }}' ? 0 : -1"
@click="tab = '{{ $id }}'">
{{ $slot }}
</button>
@@ -0,0 +1,6 @@
<div x-data="{ tab: '{{ $tab }}' }" {{ $attributes->merge(['class' => 'bg-white shadow rounded overflow-hidden']) }}>
<div class="bg-gray-100 border-b border-gray-200 flex" class="tablist" role="tablist" aria-label="{{ $label }}">
{{ $tablist }}
</div>
{{ $slot }}
</div>
+4 -4
View File
@@ -18,7 +18,7 @@
<x-filament::app-branding />
<x-filament-nav />
<x-filament::dropdown class="w-full text-left flex-grow flex items-center p-4 space-x-3 transition-colors duration-200 hover:text-white hover:bg-gray-800" id="dropdown-user">
<img src="{{ Auth::user()->avatar(32) }}" alt="{{ Auth::user()->name }}" srcset="{{ Auth::user()->avatar(32) }} 1x, {{ Auth::user()->avatar(64) }} 2x" class="flex-shrink-0 w-8 h-8 rounded-full" />
<x-filament-avatar :user="Auth::user()" :size="32" class="flex-shrink-0 w-8 h-8 rounded-full" />
<span class="flex-grow text-sm leading-tight font-semibold">{{ Auth::user()->name }}</span>
<x-slot name="content">
@@ -30,11 +30,11 @@
</ul>
</x-slot>
</x-filament::dropdown>
<button type="button" aria-controls="banner" @click.prevent="headerIsOpen = false" :aria-expanded="headerIsOpen" x-cloak x-show.opacity="headerIsOpen" class="md:hidden absolute top-0 right-0 transform translate-x-full -translate-y-2 p-3 text-gray-400 hover:text-white transition-colors duration-200">
<button type="button" aria-controls="banner" @click.prevent="headerIsOpen = false" :aria-expanded="headerIsOpen" x-cloak x-show.opacity="headerIsOpen" class="md:hidden absolute top-0 right-0 transform translate-x-full -translate-y-2 p-3 text-gray-200 hover:text-white transition-colors duration-200">
<x-heroicon-o-x class="w-6 h-6" />
</button>
</header>
<span class="absolute z-0 inset-0 bg-black opacity-50 md:hidden flex items-start justify-end" x-cloak x-show="headerIsOpen" @click="headerIsOpen = false"></span>
<span class="absolute z-0 inset-0 bg-black bg-opacity-50 md:hidden flex items-start justify-end" x-cloak x-show="headerIsOpen" @click="headerIsOpen = false"></span>
<div class="flex-grow flex flex-col">
<header class="p-4 md:p-6 flex justify-between items-center space-x-4">
<div class="flex items-center">
@@ -45,7 +45,7 @@
</div>
{{ $actions ?? null }}
</header>
<main id="content" class="flex-grow flex flex-col">
<main id="content" class="flex-grow px-4 md:px-6 flex flex-col">
{{ $slot }}
</main>
</div>
@@ -0,0 +1,29 @@
<x-filament::form submit="update">
<x-filament::input-group-stacked name="user.name" :label="__('Name')" required>
<x-filament::input name="name" wire:model="user.name" id="user.name" required tabindex="1" />
</x-filament::input-group-stacked>
<x-filament::input-group-stacked name="user.email" :label="__('E-Mail Address')" required>
<x-filament::input type="email" name="email" wire:model.lazy="user.email" id="user.email" required autocomplete="email" tabindex="2" />
</x-filament::input-group-stacked>
<x-filament::form-cols>
<x-filament::input-group-stacked name="password" :label="__('New Password')">
<x-filament::input type="password" name="password" wire:model.lazy="password" id="password" autocomplete="new-password" tabindex="3" />
<x-slot name="hint">
{{ __('Optional') }}
</x-slot>
<x-slot name="help">
{{ __('Leave blank to keep current password.') }}
</x-slot>
</x-filament::input-group-stacked>
<x-filament::input-group-stacked name="password_confirmation" :label="__('Confirm New Password')">
<x-filament::input type="password" name="password_confirmation" wire:model.lazy="password_confirmation" id="password_confirmation" autocomplete="new-password" tabindex="4" />
</x-filament::input-group-stacked>
</x-filament::form-cols>
<x-filament::button type="submit" class="btn-primary" wire:loading.attr="disabled" tabindex="5">
{{ __('Update Account') }}
</x-filament::button>
</x-filament::form>
@@ -1,4 +1,4 @@
<form wire:submit.prevent="sendEmail" class="space-y-6">
<x-filament::form submit="sendEmail">
<x-filament::input-group-stacked name="email" :label="__('E-Mail Address')" required>
<x-filament::input type="email" name="email" wire:model.defer="email" id="email" required autocomplete="email" autofocus tabindex="1" />
<x-slot name="hint">
@@ -14,4 +14,4 @@
</x-filament::button>
<x-filament::message />
</form>
</x-filament::form>
@@ -1,4 +1,4 @@
<form wire:submit.prevent="login" class="space-y-4">
<x-filament::form submit="login">
<x-filament::input-group-stacked name="email" :label="__('E-Mail Address')" required>
<x-filament::input type="email" name="email" wire:model.defer="email" id="email" required autocomplete="email" autofocus tabindex="1" />
@if (Route::has('filament.register'))
@@ -29,4 +29,4 @@
</x-filament::button>
<x-filament::message />
</form>
</x-filament::form>
@@ -1,4 +1,4 @@
<form wire:submit.prevent="register" class="space-y-4">
<x-filament::form submit="register">
<x-filament::input-group-stacked name="name" :label="__('Name')" required>
<x-filament::input name="name" wire:model.defer="name" id="name" required autofocus tabindex="1" />
<x-slot name="hint">
@@ -24,4 +24,4 @@
<x-filament::loader class="w-6 h-6 absolute left-0 ml-2 pointer-events-none" wire:loading />
{{ __('Register') }}
</x-filament::button>
</form>
</x-filament::form>
@@ -1,4 +1,4 @@
<form wire:submit.prevent="resetPassword" class="space-y-4">
<x-filament::form submit="resetPassword">
<x-filament::input-group-stacked name="email" :label="__('E-Mail Address')" required>
<x-filament::input type="email" name="email" wire:model.defer="email" id="email" required autocomplete="email" />
</x-filament::input-group-stacked>
@@ -15,4 +15,4 @@
<x-filament::loader class="w-6 h-6 absolute left-0 ml-2 pointer-events-none" wire:loading />
{{ __('Reset Password') }}
</x-filament::button>
</form>
</x-filament::form>
+3 -3
View File
@@ -1,5 +1,5 @@
<div class="p-4 md:p-6 bg-white shadow-lg">
<div class="rounded border border-dashed border-gray-300 h-64 flex items-center justify-center">
<span class="text-xs text-gray-500 font-mono">{{ config('app.name') }} Dashboard</span>
<div class="card">
<div class="p-4 border border-gray-200 border-dashed rounded-lg h-64 flex items-center justify-center">
<span class="text-xs text-gray-400 font-mono">{{ config('app.name') }} Dashboard</span>
</div>
</div>
+20 -3
View File
@@ -1,3 +1,20 @@
<div class="p-4 md:p-6 bg-white shadow-lg">
User profile
</div>
<x-filament::tabs :label="__('Profile')" tab="account">
<x-slot name="tablist">
<x-filament::tab id="account">
{{ __('Account') }}
</x-filament::tab>
@if (Filament\Features::hasPermissions())
<x-filament::tab id="permissions">
{{ __('Permissions') }}
</x-filament::tab>
@endif
</x-slot>
<x-filament::tab-panel id="account">
<livewire:filament-account :user="Auth::user()" />
</x-filament::tab-panel>
@if (Filament\Features::hasPermissions())
<x-filament::tab-panel id="permissions">
Permissions...
</x-filament::tab-panel>
@endif
</x-filament::tabs>
+20
View File
@@ -45,6 +45,16 @@ class Features
return static::enabled(static::resources());
}
/**
* Determine if the application has user permissions.
*
* @return bool
*/
public static function hasPermissions()
{
return static::enabled(static::permissions());
}
/**
* Enable the user registration feature.
*
@@ -65,6 +75,16 @@ class Features
return 'profile';
}
/**
* Enable the user permissions feature.
*
* @return string
*/
public static function permissions()
{
return 'permissions';
}
/**
* Enable the resources feature.
*
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Filament\Http\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
use Illuminate\Validation\Rule;
use Illuminate\Support\Facades\Hash;
class Account extends Component
{
use WithFileUploads;
public $user;
public $password;
public $password_confirmation;
protected $rules = [
'user.avatar' => 'nullable|image',
'user.name' => 'required|min:2',
'user.email' => [
'required',
'email',
],
'password' => 'nullable',
];
public function updated($user)
{
$this->validateOnly($user, [
'user.email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($this->user->id),
],
'password' => 'nullable|required_with:password_confirmation|min:8',
'password_confirmation' => 'nullable|same:password',
]);
}
public function update()
{
$this->validate();
if ($this->password) {
$this->user->password = Hash::make($this->password);
}
$this->user->save();
$this->reset(['password', 'password_confirmation']);
}
public function render()
{
return view('filament::livewire.account');
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ use Filament\Contracts\Resource as ResourceContract;
abstract class Resource implements ResourceContract
{
public $group = null;
public $icon = 'heroicon-o-document-text';
public $icon = 'heroicon-o-database';
public $sort = 0;
/** @return string */
-14
View File
@@ -3,7 +3,6 @@
namespace Filament\Traits;
use Illuminate\Auth\Notifications\ResetPassword;
use Thomaswelton\LaravelGravatar\Facades\Gravatar;
use Filament;
trait FilamentUser {
@@ -19,17 +18,4 @@ trait FilamentUser {
$this->notify($notification);
}
public function avatar(int $size = 48): string
{
if (!$this->avatar) {
return Gravatar::src($this->email, $size);
}
return Filament::image($this->avatar, [
'w' => $size,
'h' => $size,
'fit' => 'crop',
]);
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace Filament\View\Components;
use Illuminate\View\Component;
use Thomaswelton\LaravelGravatar\Facades\Gravatar;
use Filament;
class Avatar extends Component
{
public $user;
public $size;
public $dprs;
/**
* Create the component instance.
*
* @param object $user
* @param int $size
*
* @return void
*/
public function __construct($user, $size = 48, $dprs = [1, 2, 3])
{
$this->user = $user;
$this->size = $size;
$this->dprs = $dprs;
}
/**
* Get the view / contents that represent the component.
*
* @return \Illuminate\View\View|\Closure|string
*/
public function render()
{
return view('filament::components.avatar');
}
public function srcSet(): string
{
$srcSet = [];
foreach($this->dprs as $dpr) {
$srcSet[] = $this->src($dpr).' '.$dpr.'x';
}
return implode(', ', $srcSet);
}
public function src(int $dpr = 1): string
{
if (!$this->user->avatar) {
return Gravatar::src($this->user->email, $this->size * $dpr);
}
return Filament::image($this->user->avatar, [
'w' => $this->size,
'h' => $this->size,
'fit' => 'crop',
'dpr' => $dpr,
]);
}
}
+1
View File
@@ -1,3 +1,4 @@
// const defaultTheme = require('tailwindcss/defaultTheme')
const colors = require('tailwindcss/colors')
module.exports = {