implement user profile settings

This commit is contained in:
2026-05-01 01:07:58 +03:30
parent ff86141fd6
commit bcf9b9e47c
9 changed files with 186 additions and 21 deletions

View File

@@ -2,9 +2,11 @@
namespace App\Livewire\Settings;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts::settings')]
#[Title('Appearance settings')]
class Appearance extends Component
{

View File

@@ -7,9 +7,11 @@ use Flux\Flux;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts::settings')]
#[Title('Profile settings')]
class Profile extends Component
{

View File

@@ -13,11 +13,13 @@ use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Features;
use Laravel\Fortify\Fortify;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Title;
use Livewire\Attributes\Validate;
use Livewire\Component;
#[Layout('layouts::settings')]
#[Title('Security settings')]
class Security extends Component
{

View File

@@ -1,20 +1,12 @@
<div class="flex items-start max-md:flex-col">
<div class="me-10 w-full pb-4 md:w-[220px]">
<flux:navlist aria-label="{{ __('Settings') }}">
<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:subheading>{{ $subheading ?? '' }}</flux:subheading>
<div class="mt-5 w-full max-w-lg">
{{ $slot }}
<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="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div>
<flux:heading>{{ $heading ?? '' }}</flux:heading>
<flux:subheading>{{ $subheading ?? '' }}</flux:subheading>
</div>
</div>
<div class="mt-6 w-full max-w-2xl">
{{ $slot }}
</div>
</div>

View 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>

View File

@@ -8,7 +8,63 @@
</flux:text>
</div>
<flux:badge color="emerald" size="sm">{{ __('Live') }}</flux:badge>
<div class="flex items-center gap-2">
<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>
<flux:input

View File

@@ -21,6 +21,8 @@ test('authenticated users can visit the chat dashboard', function () {
->get(route('dashboard'))
->assertOk()
->assertSee('Inbox')
->assertSee('Profile settings')
->assertSee('Password and 2FA')
->assertDontSee('Repository')
->assertDontSee('Documentation');
});

View File

@@ -2,12 +2,21 @@
use App\Livewire\Settings\Profile;
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Livewire\Livewire;
uses(LazilyRefreshDatabase::class);
test('profile page is displayed', function () {
$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 () {

View File

@@ -2,10 +2,13 @@
use App\Livewire\Settings\Security;
use App\Models\User;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Laravel\Fortify\Features;
use Livewire\Livewire;
uses(LazilyRefreshDatabase::class);
beforeEach(function () {
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());