feat: enhance user profile update functionality and improve sidebar user display

This commit is contained in:
Yoga Pangestu 2026-07-29 00:00:19 +07:00
parent 993f3599fc
commit d053fc428c
10 changed files with 53 additions and 70 deletions

View File

@ -30,13 +30,21 @@ public function edit(Request $request): Response
*/ */
public function update(ProfileUpdateRequest $request): RedirectResponse public function update(ProfileUpdateRequest $request): RedirectResponse
{ {
$request->user()->fill($request->validated()); $validated = $request->validated();
$user = $request->user();
if ($request->user()->isDirty('email')) { $user->fill(['email' => $validated['email']]);
$request->user()->email_verified_at = null;
if ($user->isDirty('email')) {
$user->email_verified_at = null;
} }
$request->user()->save(); $user->save();
$user->userProfile()->updateOrCreate(
[],
['full_name' => $validated['name']],
);
Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]); Inertia::flash('toast', ['type' => 'success', 'message' => __('Profile updated.')]);

View File

@ -39,7 +39,7 @@ public function share(Request $request): array
...parent::share($request), ...parent::share($request),
'name' => config('app.name'), 'name' => config('app.name'),
'auth' => [ 'auth' => [
'user' => $request->user(), 'user' => $request->user()?->load('userProfile'),
], ],
'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true',
]; ];

View File

@ -7,6 +7,7 @@
use Database\Factories\UserFactory; use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Attributes\Scope; use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\HasOne;
@ -22,6 +23,8 @@ class User extends Authenticatable
{ {
use HasFactory, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable; use HasFactory, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable;
protected $with = ['userProfile'];
protected function casts(): array protected function casts(): array
{ {
return [ return [
@ -39,6 +42,13 @@ protected function active(Builder $query): void
$query->where('is_active', true); $query->where('is_active', true);
} }
protected function fullName(): Attribute
{
return Attribute::make(
get: fn() => $this->userProfile?->full_name ?? '',
);
}
public function attendances(): HasMany public function attendances(): HasMany
{ {
return $this->hasMany(Attendance::class); return $this->hasMany(Attendance::class);

View File

@ -1,4 +1,5 @@
import { Breadcrumbs } from '@/components/breadcrumbs'; import { Breadcrumbs } from '@/components/breadcrumbs';
import { NavUser } from '@/components/nav-user';
import { SidebarTrigger } from '@/components/ui/sidebar'; import { SidebarTrigger } from '@/components/ui/sidebar';
import type { BreadcrumbItem as BreadcrumbItemType } from '@/types'; import type { BreadcrumbItem as BreadcrumbItemType } from '@/types';
@ -13,6 +14,9 @@ export function AppSidebarHeader({
<SidebarTrigger className="-ml-1" /> <SidebarTrigger className="-ml-1" />
<Breadcrumbs breadcrumbs={breadcrumbs} /> <Breadcrumbs breadcrumbs={breadcrumbs} />
</div> </div>
<div className="ml-auto">
<NavUser />
</div>
</header> </header>
); );
} }

View File

@ -1,13 +1,10 @@
import { Link } from '@inertiajs/react'; import { Link } from '@inertiajs/react';
import { BookOpen, FolderGit2, LayoutGrid } from 'lucide-react'; import { LayoutGrid } from 'lucide-react';
import AppLogo from '@/components/app-logo'; import AppLogo from '@/components/app-logo';
import { NavFooter } from '@/components/nav-footer';
import { NavMain } from '@/components/nav-main'; import { NavMain } from '@/components/nav-main';
import { NavUser } from '@/components/nav-user';
import { import {
Sidebar, Sidebar,
SidebarContent, SidebarContent,
SidebarFooter,
SidebarHeader, SidebarHeader,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
@ -18,25 +15,12 @@ import type { NavItem } from '@/types';
const mainNavItems: NavItem[] = [ const mainNavItems: NavItem[] = [
{ {
title: 'Dashboard', title: 'Dasbor',
href: dashboard(), href: dashboard(),
icon: LayoutGrid, icon: LayoutGrid,
}, },
]; ];
const footerNavItems: NavItem[] = [
{
title: 'Repository',
href: 'https://github.com/laravel/react-starter-kit',
icon: FolderGit2,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#react',
icon: BookOpen,
},
];
export function AppSidebar() { export function AppSidebar() {
return ( return (
<Sidebar collapsible="icon" variant="inset"> <Sidebar collapsible="icon" variant="inset">
@ -55,11 +39,6 @@ export function AppSidebar() {
<SidebarContent> <SidebarContent>
<NavMain items={mainNavItems} /> <NavMain items={mainNavItems} />
</SidebarContent> </SidebarContent>
<SidebarFooter>
<NavFooter items={footerNavItems} className="mt-auto" />
<NavUser />
</SidebarFooter>
</Sidebar> </Sidebar>
); );
} }

View File

@ -1,10 +1,9 @@
import { Link } from '@inertiajs/react'; import { Link } from '@inertiajs/react';
import { import {
SidebarGroup, SidebarGroup,
SidebarGroupLabel,
SidebarMenu, SidebarMenu,
SidebarMenuButton, SidebarMenuButton,
SidebarMenuItem, SidebarMenuItem
} from '@/components/ui/sidebar'; } from '@/components/ui/sidebar';
import { useCurrentUrl } from '@/hooks/use-current-url'; import { useCurrentUrl } from '@/hooks/use-current-url';
import type { NavItem } from '@/types'; import type { NavItem } from '@/types';
@ -14,7 +13,6 @@ export function NavMain({ items = [] }: { items: NavItem[] }) {
return ( return (
<SidebarGroup className="px-2 py-0"> <SidebarGroup className="px-2 py-0">
<SidebarGroupLabel>Platform</SidebarGroupLabel>
<SidebarMenu> <SidebarMenu>
{items.map((item) => ( {items.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>

View File

@ -5,54 +5,33 @@ import {
DropdownMenuContent, DropdownMenuContent,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from '@/components/ui/sidebar';
import { UserInfo } from '@/components/user-info'; import { UserInfo } from '@/components/user-info';
import { UserMenuContent } from '@/components/user-menu-content'; import { UserMenuContent } from '@/components/user-menu-content';
import { useIsMobile } from '@/hooks/use-mobile';
export function NavUser() { export function NavUser() {
const { auth } = usePage().props; const { auth } = usePage().props;
const { state } = useSidebar();
const isMobile = useIsMobile();
if (!auth.user) { if (!auth.user) {
return null; return null;
} }
return ( return (
<SidebarMenu> <DropdownMenu>
<SidebarMenuItem> <DropdownMenuTrigger asChild>
<DropdownMenu> <button
<DropdownMenuTrigger asChild> className="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
<SidebarMenuButton data-test="header-user-menu-button"
size="lg" >
className="group text-sidebar-accent-foreground data-[state=open]:bg-sidebar-accent" <UserInfo user={auth.user} />
data-test="sidebar-menu-button" <ChevronsUpDown className="ml-auto size-4" />
> </button>
<UserInfo user={auth.user} /> </DropdownMenuTrigger>
<ChevronsUpDown className="ml-auto size-4" /> <DropdownMenuContent
</SidebarMenuButton> className="w-56 rounded-lg"
</DropdownMenuTrigger> align="end"
<DropdownMenuContent >
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg" <UserMenuContent user={auth.user} />
align="end" </DropdownMenuContent>
side={ </DropdownMenu>
isMobile
? 'bottom'
: state === 'collapsed'
? 'left'
: 'bottom'
}
>
<UserMenuContent user={auth.user} />
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
); );
} }

View File

@ -8,6 +8,10 @@ function getInitial(name: string): string {
export function useInitials(): GetInitialsFn { export function useInitials(): GetInitialsFn {
return useCallback((fullName: string): string => { return useCallback((fullName: string): string => {
if (!fullName) {
return '';
}
const names = fullName.trim().split(/\s+/u).filter(Boolean); const names = fullName.trim().split(/\s+/u).filter(Boolean);
if (names.length === 0) { if (names.length === 0) {

View File

@ -12,6 +12,7 @@ export function useIsMobile() {
} }
mql.addEventListener("change", onChange) mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange) return () => mql.removeEventListener("change", onChange)
}, []) }, [])

View File

@ -1,3 +1,4 @@
import { Form, Head } from '@inertiajs/react';
import InputError from '@/components/input-error'; import InputError from '@/components/input-error';
import PasswordInput from '@/components/password-input'; import PasswordInput from '@/components/password-input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -6,7 +7,6 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Spinner } from '@/components/ui/spinner'; import { Spinner } from '@/components/ui/spinner';
import { store } from '@/routes/login'; import { store } from '@/routes/login';
import { Form, Head } from '@inertiajs/react';
export default function Login() { export default function Login() {
return ( return (