implement user profile settings
This commit is contained in:
@@ -2,9 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Settings;
|
namespace App\Livewire\Settings;
|
||||||
|
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts::settings')]
|
||||||
#[Title('Appearance settings')]
|
#[Title('Appearance settings')]
|
||||||
class Appearance extends Component
|
class Appearance extends Component
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ use Flux\Flux;
|
|||||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts::settings')]
|
||||||
#[Title('Profile settings')]
|
#[Title('Profile settings')]
|
||||||
class Profile extends Component
|
class Profile extends Component
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
|
|||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
use Laravel\Fortify\Fortify;
|
use Laravel\Fortify\Fortify;
|
||||||
use Livewire\Attributes\Computed;
|
use Livewire\Attributes\Computed;
|
||||||
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Attributes\Validate;
|
use Livewire\Attributes\Validate;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
|
#[Layout('layouts::settings')]
|
||||||
#[Title('Security settings')]
|
#[Title('Security settings')]
|
||||||
class Security extends Component
|
class Security extends Component
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,20 +1,12 @@
|
|||||||
<div class="flex items-start max-md:flex-col">
|
<div class="rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-800 dark:bg-zinc-900 sm:p-6">
|
||||||
<div class="me-10 w-full pb-4 md:w-[220px]">
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<flux:navlist aria-label="{{ __('Settings') }}">
|
<div>
|
||||||
<flux:navlist.item :href="route('profile.edit')" wire:navigate>{{ __('Profile') }}</flux:navlist.item>
|
|
||||||
<flux:navlist.item :href="route('security.edit')" wire:navigate>{{ __('Security') }}</flux:navlist.item>
|
|
||||||
<flux:navlist.item :href="route('appearance.edit')" wire:navigate>{{ __('Appearance') }}</flux:navlist.item>
|
|
||||||
</flux:navlist>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<flux:separator class="md:hidden" />
|
|
||||||
|
|
||||||
<div class="flex-1 self-stretch max-md:pt-6">
|
|
||||||
<flux:heading>{{ $heading ?? '' }}</flux:heading>
|
<flux:heading>{{ $heading ?? '' }}</flux:heading>
|
||||||
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
|
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 w-full max-w-lg">
|
<div class="mt-6 w-full max-w-2xl">
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|||||||
97
resources/views/layouts/settings.blade.php
Normal file
97
resources/views/layouts/settings.blade.php
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
|
||||||
|
<head>
|
||||||
|
@include('partials.head')
|
||||||
|
</head>
|
||||||
|
<body class="min-h-dvh bg-zinc-100 text-zinc-950 antialiased dark:bg-zinc-950 dark:text-zinc-50">
|
||||||
|
<div class="min-h-dvh lg:grid lg:grid-cols-[18rem_1fr]">
|
||||||
|
<aside class="border-b border-zinc-200 bg-white/90 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/80 lg:sticky lg:top-0 lg:h-dvh lg:border-b-0 lg:border-e">
|
||||||
|
<div class="flex h-full flex-col gap-6 p-4 lg:p-5">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<a href="{{ route('dashboard') }}" wire:navigate class="flex min-w-0 items-center gap-3 rounded-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-900">
|
||||||
|
<span class="flex size-9 shrink-0 items-center justify-center rounded-lg bg-zinc-950 text-white dark:bg-white dark:text-zinc-950">
|
||||||
|
<x-app-logo-icon class="size-5 fill-current" />
|
||||||
|
</span>
|
||||||
|
<span class="truncate text-sm font-semibold">{{ __('Fluent Chat') }}</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<flux:button
|
||||||
|
:href="route('dashboard')"
|
||||||
|
wire:navigate
|
||||||
|
variant="ghost"
|
||||||
|
icon="x-mark"
|
||||||
|
aria-label="{{ __('Back to chat') }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<flux:heading size="xl">{{ __('Account') }}</flux:heading>
|
||||||
|
<flux:text class="mt-2 text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
|
{{ __('Manage your profile, password, and sign-in security.') }}
|
||||||
|
</flux:text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:navlist aria-label="{{ __('Account settings') }}" class="grid gap-1">
|
||||||
|
<flux:navlist.item
|
||||||
|
:href="route('profile.edit')"
|
||||||
|
:current="request()->routeIs('profile.edit')"
|
||||||
|
icon="user-circle"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('Profile') }}
|
||||||
|
</flux:navlist.item>
|
||||||
|
|
||||||
|
<flux:navlist.item
|
||||||
|
:href="route('security.edit')"
|
||||||
|
:current="request()->routeIs('security.edit')"
|
||||||
|
icon="shield-check"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('Security') }}
|
||||||
|
</flux:navlist.item>
|
||||||
|
|
||||||
|
<flux:navlist.item
|
||||||
|
:href="route('appearance.edit')"
|
||||||
|
:current="request()->routeIs('appearance.edit')"
|
||||||
|
icon="swatch"
|
||||||
|
wire:navigate
|
||||||
|
>
|
||||||
|
{{ __('Appearance') }}
|
||||||
|
</flux:navlist.item>
|
||||||
|
</flux:navlist>
|
||||||
|
|
||||||
|
<div class="mt-auto hidden lg:block">
|
||||||
|
<div class="rounded-lg border border-zinc-200 bg-zinc-50 p-3 dark:border-zinc-800 dark:bg-zinc-950">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<flux:avatar
|
||||||
|
circle
|
||||||
|
:name="auth()->user()->name"
|
||||||
|
:initials="auth()->user()->initials()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="truncate text-sm font-medium">{{ auth()->user()->name }}</div>
|
||||||
|
<div class="truncate text-xs text-zinc-500 dark:text-zinc-400">{{ auth()->user()->email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="min-w-0 px-4 py-6 sm:px-6 lg:px-10 lg:py-10">
|
||||||
|
<div class="mx-auto w-full max-w-4xl">
|
||||||
|
{{ $slot }}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@persist('toast')
|
||||||
|
<flux:toast.group>
|
||||||
|
<flux:toast />
|
||||||
|
</flux:toast.group>
|
||||||
|
@endpersist
|
||||||
|
|
||||||
|
@fluxScripts
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -8,7 +8,63 @@
|
|||||||
</flux:text>
|
</flux:text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<flux:badge color="emerald" size="sm">{{ __('Live') }}</flux:badge>
|
<flux:badge color="emerald" size="sm">{{ __('Live') }}</flux:badge>
|
||||||
|
|
||||||
|
<flux:dropdown position="bottom" align="end">
|
||||||
|
<flux:profile
|
||||||
|
:name="auth()->user()->name"
|
||||||
|
:initials="auth()->user()->initials()"
|
||||||
|
icon-trailing="chevron-down"
|
||||||
|
aria-label="{{ __('Account menu') }}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<flux:menu>
|
||||||
|
<div class="p-2">
|
||||||
|
<div class="flex items-center gap-3 rounded-lg bg-zinc-50 p-2 dark:bg-zinc-900">
|
||||||
|
<flux:avatar
|
||||||
|
circle
|
||||||
|
:name="auth()->user()->name"
|
||||||
|
:initials="auth()->user()->initials()"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="min-w-0">
|
||||||
|
<div class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">{{ auth()->user()->name }}</div>
|
||||||
|
<div class="truncate text-xs text-zinc-500 dark:text-zinc-400">{{ auth()->user()->email }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<flux:menu.separator />
|
||||||
|
|
||||||
|
<flux:menu.item :href="route('profile.edit')" icon="user-circle" wire:navigate>
|
||||||
|
{{ __('Profile settings') }}
|
||||||
|
</flux:menu.item>
|
||||||
|
|
||||||
|
<flux:menu.item :href="route('security.edit')" icon="shield-check" wire:navigate>
|
||||||
|
{{ __('Password and 2FA') }}
|
||||||
|
</flux:menu.item>
|
||||||
|
|
||||||
|
<flux:menu.item :href="route('appearance.edit')" icon="swatch" wire:navigate>
|
||||||
|
{{ __('Appearance') }}
|
||||||
|
</flux:menu.item>
|
||||||
|
|
||||||
|
<flux:menu.separator />
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('logout') }}" class="w-full">
|
||||||
|
@csrf
|
||||||
|
<flux:menu.item
|
||||||
|
as="button"
|
||||||
|
type="submit"
|
||||||
|
icon="arrow-right-start-on-rectangle"
|
||||||
|
class="w-full cursor-pointer"
|
||||||
|
>
|
||||||
|
{{ __('Log out') }}
|
||||||
|
</flux:menu.item>
|
||||||
|
</form>
|
||||||
|
</flux:menu>
|
||||||
|
</flux:dropdown>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<flux:input
|
<flux:input
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ test('authenticated users can visit the chat dashboard', function () {
|
|||||||
->get(route('dashboard'))
|
->get(route('dashboard'))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Inbox')
|
->assertSee('Inbox')
|
||||||
|
->assertSee('Profile settings')
|
||||||
|
->assertSee('Password and 2FA')
|
||||||
->assertDontSee('Repository')
|
->assertDontSee('Repository')
|
||||||
->assertDontSee('Documentation');
|
->assertDontSee('Documentation');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,21 @@
|
|||||||
|
|
||||||
use App\Livewire\Settings\Profile;
|
use App\Livewire\Settings\Profile;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(LazilyRefreshDatabase::class);
|
||||||
|
|
||||||
test('profile page is displayed', function () {
|
test('profile page is displayed', function () {
|
||||||
$this->actingAs($user = User::factory()->create());
|
$this->actingAs($user = User::factory()->create());
|
||||||
|
|
||||||
$this->get('/settings/profile')->assertOk();
|
$this->get('/settings/profile')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Profile')
|
||||||
|
->assertSee('Name')
|
||||||
|
->assertSee('Email')
|
||||||
|
->assertDontSee('Repository')
|
||||||
|
->assertDontSee('Documentation');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('profile information can be updated', function () {
|
test('profile information can be updated', function () {
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
use App\Livewire\Settings\Security;
|
use App\Livewire\Settings\Security;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Laravel\Fortify\Features;
|
use Laravel\Fortify\Features;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(LazilyRefreshDatabase::class);
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user