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
{
$request->user()->fill($request->validated());
$validated = $request->validated();
$user = $request->user();
if ($request->user()->isDirty('email')) {
$request->user()->email_verified_at = null;
$user->fill(['email' => $validated['email']]);
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.')]);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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