feature: update avatar
This commit is contained in:
@@ -71,20 +71,35 @@ class UserController extends Controller
|
|||||||
return view('user.change_password');
|
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([
|
$request->validate([
|
||||||
'password' => 'required|min:6|confirmed',
|
'avatar' => 'nullable|image|max:2048', // 2MB max
|
||||||
], [
|
'new_password' => 'nullable|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.',
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
$this->userService->updatePassword($user, $request->password);
|
|
||||||
|
|
||||||
$route = $user->role == config('constants.ROLE_ADMIN') ? 'admin.dashboard' : 'user.dashboard';
|
// Avatar handling
|
||||||
return redirect()->route($route)->with('success', __('messages.password_change_success'));
|
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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,4 +67,26 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return $this->hasMany(AddCard::class, 'buyer', 'msnv');
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
</x-sidebar-item>
|
</x-sidebar-item>
|
||||||
@endif
|
@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
|
Cài đặt
|
||||||
</x-sidebar-item>
|
</x-sidebar-item>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
'create' => route('admin.users.store'),
|
'create' => route('admin.users.store'),
|
||||||
'edit' => route('admin.users.update', $user?->msnv),
|
'edit' => route('admin.users.update', $user?->msnv),
|
||||||
'delete' => route('admin.users.destroy', $user?->msnv),
|
'delete' => route('admin.users.destroy', $user?->msnv),
|
||||||
|
'self' => route('user.update_profile'),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -55,4 +55,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
Route::get('/change-password', [UserController::class, 'changePasswordForm'])->name('user.change_password');
|
Route::get('/change-password', [UserController::class, 'changePasswordForm'])->name('user.change_password');
|
||||||
Route::post('/change-password', [UserController::class, 'updatePassword'])->name('user.update_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');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user