feature: update avatar

This commit is contained in:
antv
2026-07-09 09:05:57 +07:00
parent 439eec9015
commit 25bb3d961e
6 changed files with 129 additions and 10 deletions
+24 -9
View File
@@ -71,20 +71,35 @@ class UserController extends Controller
return view('user.change_password');
}
public function updatePassword(Request $request)
// Show edit profile page (avatar & password)
public function editProfile()
{
return view('user.edit');
}
// Handle avatar upload and optional password change
public function updateProfile(Request $request)
{
$request->validate([
'password' => 'required|min:6|confirmed',
], [
'password.required' => 'Mật khẩu mới không được để trống.',
'password.min' => 'Mật khẩu mới phải có ít nhất 6 ký tự.',
'password.confirmed' => 'Mật khẩu xác nhận không trùng khớp.',
'avatar' => 'nullable|image|max:2048', // 2MB max
'new_password' => 'nullable|min:6|confirmed',
]);
$user = Auth::user();
$this->userService->updatePassword($user, $request->password);
$route = $user->role == config('constants.ROLE_ADMIN') ? 'admin.dashboard' : 'user.dashboard';
return redirect()->route($route)->with('success', __('messages.password_change_success'));
// Avatar handling
if ($request->hasFile('avatar')) {
$path = $request->file('avatar')->store('avatars', 'public');
$user->avatar = 'storage/' . $path;
}
// Password handling (optional)
if ($request->filled('new_password')) {
$this->userService->updatePassword($user, $request->new_password);
}
$user->save();
return redirect()->route('user.edit')->with('success', 'Cập nhật hồ sơ thành công');
}
}
+22
View File
@@ -67,4 +67,26 @@ class User extends Authenticatable
{
return $this->hasMany(AddCard::class, 'buyer', 'msnv');
}
/**
* Get the user's avatar URL.
* If the user doesn't have an avatar, returns the default avatar.
*
* @param string|null $value
* @return string
*/
public function getAvatarAttribute($value)
{
if (!$value) {
return asset('images/avatar.png');
}
// Return direct URL if it's already an absolute URL (for backward compatibility)
if (filter_var($value, FILTER_VALIDATE_URL)) {
return $value;
}
return asset($value);
}
}
+1 -1
View File
@@ -38,7 +38,7 @@
</x-sidebar-item>
@endif
<x-sidebar-item href="{{ route('user.change_password') }}" :active="request()->routeIs('user.change_password')" icon="gear">
<x-sidebar-item href="{{ route('user.edit') }}" :active="request()->routeIs('user.edit')" icon="gear">
Cài đặt
</x-sidebar-item>
</nav>
@@ -10,6 +10,7 @@
'create' => route('admin.users.store'),
'edit' => route('admin.users.update', $user?->msnv),
'delete' => route('admin.users.destroy', $user?->msnv),
'self' => route('user.update_profile'),
default => null,
};
+78
View File
@@ -0,0 +1,78 @@
@extends('layouts.app')
@section('title', 'Cài đặt tài khoản')
@section('content')
<div class="max-w-[480px] mx-auto bg-white rounded-[24px] shadow-[0_10px_30px_rgba(15,23,42,0.05)] p-8 mt-6">
<h3 class="text-[20px] font-extrabold text-[#1a2b49] mb-4 text-center">Cài đặt tài khoản</h3>
@if(session('success'))
<div class="mb-4 p-3 bg-green-100 text-green-800 rounded-xl">{{ session('success') }}</div>
@endif
<form action="{{ route('user.update_profile') }}" method="POST" enctype="multipart/form-data">
@csrf
<!-- Avatar preview and upload -->
<div class="flex items-center gap-4 mb-6">
<div class="w-16 h-16 rounded-full overflow-hidden border border-gray-200">
<img src="{{ Auth::user()->avatar ?? asset('images/avatar.png') }}" alt="Avatar" class="w-full h-full object-cover" id="avatar-preview">
</div>
<div class="flex-1">
<label class="block text-[13px] font-bold text-gray-700 mb-1" for="avatar">Thay đổi ảnh đại diện</label>
<input type="file" name="avatar" id="avatar" accept="image/*" class="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-3 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-[#3462f7] file:text-white hover:file:bg-[#254edb]" onchange="previewAvatar(event)">
</div>
</div>
<!-- New password -->
<div class="space-y-6 mb-4">
<div class="flex flex-col">
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="new_password">Mật khẩu mới <span class="text-red-500">*</span></label>
<div class="relative">
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-5 h-5\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M16.5 10.5V6.75a4.5 4.5 0 10-9 0V10.5m-2.812 10.5h14.625c.621 0 1.125-.504 1.125-1.125V11.25c0-.621-.504-1.125-1.125-1.125H3.75c-.621 0-1.125.504-1.125 1.125v7.875c0 .621.504 1.125 1.125 1.125z\"/></svg></span>
<input type="password" name="new_password" id="new_password" class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('new_password') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#2563eb] focus:ring-[#2563eb]/20 @enderror rounded-lg text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400" placeholder="Nhập mật khẩu mới">
<button type="button" onclick="togglePasswordVisibility('new_password', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</button>
</div>
@error('new_password')
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
@enderror
</div>
<div class="flex flex-col">
<label class="text-[13px] font-bold text-gray-700 mb-1.5" for="new_password_confirmation">Xác nhận mật khẩu mới <span class="text-red-500">*</span></label>
<div class="relative">
<span class="absolute left-3.5 top-1/2 -translate-y-1/2 text-gray-400"><svg xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"2\" stroke=\"currentColor\" class=\"w-5 h-5\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg></span>
<input type="password" name="new_password_confirmation" id="new_password_confirmation" class="w-full h-[46px] pl-[42px] pr-12 bg-white border @error('new_password_confirmation') border-red-500 focus:border-red-500 focus:ring-red-500/20 @else border-gray-200 focus:border-[#2563eb] focus:ring-[#2563eb]/20 @enderror rounded-lg text-sm text-gray-800 outline-none transition-all shadow-sm placeholder:text-gray-400" placeholder="Nhập lại mật khẩu mới">
<button type="button" onclick="togglePasswordVisibility('new_password_confirmation', this)" class="absolute right-3.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 focus:outline-none cursor-pointer" aria-label="Show password">
<svg class="w-5 h-5 eye-icon-closed" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12c1.292 4.338 5.31 7.5 10.066 7.5.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"/></svg>
<svg class="w-5 h-5 eye-icon-open hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"/><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/></svg>
</button>
</div>
@error('new_password_confirmation')
<span class="text-red-500 text-[12px] font-semibold mt-1.5 block">{{ $message }}</span>
@enderror
</div>
</div>
<div class="flex items-center gap-3 pt-4">
<a href="{{ route('user.dashboard') }}" class="flex-1 text-center py-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-xl font-medium">Hủy bỏ</a>
<button type="submit" class="flex-1 py-2 bg-[#3462f7] hover:bg-[#254edb] text-white rounded-xl font-bold">Lưu thay đổi</button>
</div>
</form>
</div>
<script>
function previewAvatar(event) {
const input = event.target;
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
document.getElementById('avatar-preview').src = e.target.result;
};
reader.readAsDataURL(input.files[0]);
}
}
</script>
@endsection
+3
View File
@@ -55,4 +55,7 @@ Route::middleware('auth')->group(function () {
Route::get('/change-password', [UserController::class, 'changePasswordForm'])->name('user.change_password');
Route::post('/change-password', [UserController::class, 'updatePassword'])->name('user.update_password');
// Edit personal profile (avatar & password)
Route::get('/profile/edit', [UserController::class, 'editProfile'])->name('user.edit');
Route::post('/profile/edit', [UserController::class, 'updateProfile'])->name('user.update_profile');
});