mirror of
https://github.com/filamentphp/filament.git
synced 2026-09-24 15:42:09 +08:00
Start on user profile / permissions.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
/*
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.card {
|
||||
@apply p-4 md:p-6 bg-white shadow rounded overflow-hidden;
|
||||
}
|
||||
@@ -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,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,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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
// const defaultTheme = require('tailwindcss/defaultTheme')
|
||||
const colors = require('tailwindcss/colors')
|
||||
|
||||
module.exports = {
|
||||
|
||||
Reference in New Issue
Block a user