Обновление пользователя.

This commit is contained in:
Toy Rik 2026-02-23 12:14:58 +03:00
parent b9fd0b70c7
commit 8c46816ff8
15 changed files with 540 additions and 6 deletions

View File

@ -19,7 +19,7 @@ class ShowAction
public function execute(ShowRequest $request) public function execute(ShowRequest $request)
{ {
$result = $this->userRepository->whereUuid($request->user_uuid)->firstOrFail(); $result = $this->userRepository->with('role')->whereUuid($request->user_uuid)->firstOrFail();
return ShowResponseData::fromModel($result); return ShowResponseData::fromModel($result);
} }
} }

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Domain\User\Actions;
use App\Domain\User\Data\UpdateRequest;
use App\Domain\User\Repositories\UserRepository;
use Illuminate\Http\JsonResponse;
class UpdateAction
{
public function __construct(
private UserRepository $userRepository,
) {
}
public function execute(UpdateRequest $request)
{
$user = $this->userRepository->whereUuid($request->user_uuid)->firstOrFail();
$user->update($request->getFilledFields());
}
}

View File

@ -13,6 +13,7 @@ class ShowResponseData extends Data
public function __construct( public function __construct(
public readonly string $uuid, public readonly string $uuid,
public readonly string $name, public readonly string $name,
public readonly string $email,
public readonly RoleData $role, public readonly RoleData $role,
) { ) {
} }
@ -22,6 +23,7 @@ class ShowResponseData extends Data
return new self( return new self(
uuid: $model->uuid->toString(), uuid: $model->uuid->toString(),
name: $model->name, name: $model->name,
email: $model->email,
role: new RoleData( role: new RoleData(
uuid: $model->role->uuid->toString(), uuid: $model->role->uuid->toString(),
name: $model->role->name, name: $model->role->name,

View File

@ -2,13 +2,41 @@
declare(strict_types=1); declare(strict_types=1);
use Spatie\LaravelData\Data; namespace App\Domain\User\Data;
use Spatie\LaravelData\Attributes\FromRouteParameter;
use Spatie\LaravelData\Attributes\MapName;
use Spatie\LaravelData\Attributes\Validation\Email;
use Spatie\LaravelData\Attributes\Validation\Exists;
use Spatie\LaravelData\Attributes\Validation\StringType;
use Spatie\LaravelData\Attributes\Validation\Nullable;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapName(SnakeCaseMapper::class)]
class UpdateRequest extends Data class UpdateRequest extends Data
{ {
public function __construct( public function __construct(
#[StringType, FromRouteParameter('user_uuid'), Exists('users', 'uuid')]
public readonly string $user_uuid, public readonly string $user_uuid,
#[StringType, Nullable]
public readonly ?string $name,
#[StringType, Email, Nullable]
public readonly ?string $email,
#[StringType, Nullable]
public readonly ?string $password,
) { ) {
} }
public function getFilledFields(): array
{
$fields = [
'name' => $this->name,
'email' => $this->email,
'password' => $this->password ? bcrypt($this->password) : null,
];
return array_filter($fields, fn($value) => $value !== null);
}
} }

View File

@ -49,7 +49,8 @@ class MenuHelper
public static function getIconSvg($iconName) public static function getIconSvg($iconName)
{ {
$icons = [ $icons = [
'dashboard' => '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M5.5 3.25C4.25736 3.25 3.25 4.25736 3.25 5.5V8.99998C3.25 10.2426 4.25736 11.25 5.5 11.25H9C10.2426 11.25 11.25 10.2426 11.25 8.99998V5.5C11.25 4.25736 10.2426 3.25 9 3.25H5.5ZM4.75 5.5C4.75 5.08579 5.08579 4.75 5.5 4.75H9C9.41421 4.75 9.75 5.08579 9.75 5.5V8.99998C9.75 9.41419 9.41421 9.74998 9 9.74998H5.5C5.08579 9.74998 4.75 9.41419 4.75 8.99998V5.5ZM5.5 12.75C4.25736 12.75 3.25 13.7574 3.25 15V18.5C3.25 19.7426 4.25736 20.75 5.5 20.75H9C10.2426 20.75 11.25 19.7427 11.25 18.5V15C11.25 13.7574 10.2426 12.75 9 12.75H5.5ZM4.75 15C4.75 14.5858 5.08579 14.25 5.5 14.25H9C9.41421 14.25 9.75 14.5858 9.75 15V18.5C9.75 18.9142 9.41421 19.25 9 19.25H5.5C5.08579 19.25 4.75 18.9142 4.75 18.5V15ZM12.75 5.5C12.75 4.25736 13.7574 3.25 15 3.25H18.5C19.7426 3.25 20.75 4.25736 20.75 5.5V8.99998C20.75 10.2426 19.7426 11.25 18.5 11.25H15C13.7574 11.25 12.75 10.2426 12.75 8.99998V5.5ZM15 4.75C14.5858 4.75 14.25 5.08579 14.25 5.5V8.99998C14.25 9.41419 14.5858 9.74998 15 9.74998H18.5C18.9142 9.74998 19.25 9.41419 19.25 8.99998V5.5C19.25 5.08579 18.9142 4.75 18.5 4.75H15ZM15 12.75C13.7574 12.75 12.75 13.7574 12.75 15V18.5C12.75 19.7426 13.7574 20.75 15 20.75H18.5C19.7426 20.75 20.75 19.7427 20.75 18.5V15C20.75 13.7574 19.7426 12.75 18.5 12.75H15ZM14.25 15C14.25 14.5858 14.5858 14.25 15 14.25H18.5C18.9142 14.25 19.25 14.5858 19.25 15V18.5C19.25 18.9142 18.9142 19.25 18.5 19.25H15C14.5858 19.25 14.25 18.9142 14.25 18.5V15Z" fill="currentColor"></path></svg>', 'dashboard' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>Dashboard SVG Icon</title><path fill="currentColor" d="M24 21h2v5h-2zm-4-5h2v10h-2zm-9 10a5.006 5.006 0 0 1-5-5h2a3 3 0 1 0 3-3v-2a5 5 0 0 1 0 10"/><path fill="currentColor" d="M28 2H4a2.002 2.002 0 0 0-2 2v24a2.002 2.002 0 0 0 2 2h24a2.003 2.003 0 0 0 2-2V4a2.002 2.002 0 0 0-2-2m0 9H14V4h14ZM12 4v7H4V4ZM4 28V13h24l.002 15Z"/></svg>',
'user-profile' => '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><title>User-avatar SVG Icon</title><path fill="currentColor" d="M16 8a5 5 0 1 0 5 5a5 5 0 0 0-5-5m0 8a3 3 0 1 1 3-3a3.003 3.003 0 0 1-3 3"/><path fill="currentColor" d="M16 2a14 14 0 1 0 14 14A14.016 14.016 0 0 0 16 2m-6 24.377V25a3.003 3.003 0 0 1 3-3h6a3.003 3.003 0 0 1 3 3v1.377a11.899 11.899 0 0 1-12 0m13.993-1.451A5.002 5.002 0 0 0 19 20h-6a5.002 5.002 0 0 0-4.992 4.926a12 12 0 1 1 15.985 0"/></svg>'
]; ];
return $icons[$iconName] ?? '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>'; return $icons[$iconName] ?? '<svg width="1em" height="1em" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" fill="currentColor"/></svg>';
} }

View File

@ -5,7 +5,9 @@ declare(strict_types=1);
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Domain\User\Actions\ShowAction; use App\Domain\User\Actions\ShowAction;
use App\Domain\User\Actions\UpdateAction;
use App\Domain\User\Data\ShowRequest; use App\Domain\User\Data\ShowRequest;
use App\Domain\User\Data\UpdateRequest;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
class UserController extends Controller class UserController extends Controller
@ -20,4 +22,11 @@ class UserController extends Controller
$data = $action->execute($request); $data = $action->execute($request);
return view('pages.user.show', ['data' => $data]); return view('pages.user.show', ['data' => $data]);
} }
public function update(UpdateRequest $request, UpdateAction $action)
{
$data = $action->execute($request);
return response()->json(['success' => true, 'message' => 'Profile updated successfully']);
}
} }

16
src/lang/ru.json Normal file
View File

@ -0,0 +1,16 @@
{
"Phone": "Телефон",
"Log in": "Войти",
"Profile": "Профиль",
"User Profile": "Профиль Пользователя",
"Edit": "Редактировать",
"Personal Information": "Персональная информация",
"Update your details to keep your profile up-to-date.": "Обновите свои данные, чтобы поддерживать актуальность вашего профиля.",
"Name": "Имя",
"Email Address": "Адрес электронной почты",
"Sign Up": "Зарегистрироваться",
"Reset Password": "Сбросить пароль",
"Password": "Пароль",
"Reset": "Сбросить",
"Verification link sent! ": ""
}

4
src/lang/ru/auth.php Normal file
View File

@ -0,0 +1,4 @@
<?php
return [
'failed' => 'Неверный логин или пароль',
];

View File

@ -0,0 +1,17 @@
<?php
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
return [
'next' => 'Вперёд &raquo;',
'previous' => '&laquo; Назад',
];

22
src/lang/ru/passwords.php Normal file
View File

@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => 'Ваш пароль был сброшен.',
'sent' => 'Мы отправили вам по электронной почте ссылку для сброса пароля.',
'throttled' => 'Пожалуйста, подождите, прежде чем повторить попытку.',
'token' => 'Этот токен сброса пароля недействителен.',
'user' => "Пользователь с таким адресом электронной почты не найден.",
];

137
src/lang/ru/validation.php Normal file
View File

@ -0,0 +1,137 @@
<?php
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
return [
'accepted' => 'Вы должны принять ":attribute".',
'accepted_if' => 'Вы должны принять ":attribute", когда :other соответствует :value.',
'active_url' => 'Значение поля ":attribute" не является действительным URL.',
'after' => 'Значение поля ":attribute" должно быть датой после :date.',
'after_or_equal' => 'Значение поля ":attribute" должно быть датой после или равной :date.',
'alpha' => 'Значение поля ":attribute" может содержать только буквы.',
'alpha_dash' => 'Значение поля ":attribute" может содержать только буквы, цифры, дефис и нижнее подчеркивание.',
'alpha_num' => 'Значение поля ":attribute" может содержать только буквы и цифры.',
'array' => 'Значение поля ":attribute" должно быть массивом.',
'before' => 'Значение поля ":attribute" должно быть датой до :date.',
'before_or_equal' => 'Значение поля ":attribute" должно быть датой до или равной :date.',
'between' => [
'array' => 'Количество элементов в поле ":attribute" должно быть между :min и :max.',
'file' => 'Размер файла в поле ":attribute" должен быть между :min и :max Килобайт(а).',
'numeric' => 'Значение поля ":attribute" должно быть между :min и :max.',
'string' => 'Количество символов в поле ":attribute" должно быть между :min и :max.',
],
'boolean' => 'Значение поля ":attribute" должно быть логического типа.',
'confirmed' => 'Значение поля ":attribute" не совпадает с подтверждаемым.',
'current_password' => 'Неверный пароль.',
'date' => 'Значение поля ":attribute" не является датой.',
'date_equals' => 'Значение поля ":attribute" должно быть датой равной :date.',
'date_format' => 'Значение поля ":attribute" не соответствует формату даты :format.',
'declined' => 'Поле ":attribute" должно быть отклонено.',
'declined_if' => 'Поле ":attribute" должно быть отклонено, когда :other равно :value.',
'different' => 'Значения полей ":attribute" и :other должны различаться.',
'digits' => 'Длина значения цифрового поля ":attribute" должна быть :digits.',
'digits_between' => 'Длина значения цифрового поля ":attribute" должна быть между :min и :max.',
'dimensions' => 'Изображение в поле ":attribute" имеет недопустимые размеры.',
'distinct' => 'Значения поля ":attribute" не должны повторяться.',
'email' => 'Значение поля ":attribute" должно быть действительным электронным адресом.',
'ends_with' => 'Поле ":attribute" должно заканчиваться одним из следующих значений: :values',
'enum' => 'Выбранное значение для ":attribute" некорректно.',
'exists' => 'Выбранное значение для ":attribute" некорректно.',
'file' => 'В поле ":attribute" должен быть указан файл.',
'filled' => 'Поле ":attribute" обязательно для заполнения.',
'gt' => [
'array' => 'Количество элементов в поле ":attribute" должно быть больше :value.',
'file' => 'Размер файла в поле ":attribute" должен быть больше :value Килобайт(а).',
'numeric' => 'Значение поля ":attribute" должно быть больше :value.',
'string' => 'Количество символов в поле ":attribute" должно быть больше :value.',
],
'gte' => [
'array' => 'Количество элементов в поле ":attribute" должно быть :value или больше.',
'file' => 'Размер файла в поле ":attribute" должен быть :value Килобайт(а) или больше.',
'numeric' => 'Значение поля ":attribute" должно быть :value или больше.',
'string' => 'Количество символов в поле ":attribute" должно быть :value или больше.',
],
'image' => 'Файл в поле ":attribute" должен быть изображением.',
'in' => 'Выбранное значение для ":attribute" некорректно.',
'in_array' => 'Значение поля ":attribute" не существует в :other.',
'integer' => 'Значение поля ":attribute" должно быть целым числом.',
'ip' => 'Значение поля ":attribute" должно быть действительным IP-адресом.',
'ipv4' => 'Значение поля ":attribute" должно быть действительным IPv4-адресом.',
'ipv6' => 'Значение поля ":attribute" должно быть действительным IPv6-адресом.',
'json' => 'Значение поля ":attribute" должно быть JSON строкой.',
'lt' => [
'array' => 'Количество элементов в поле ":attribute" должно быть меньше :value.',
'file' => 'Размер файла в поле ":attribute" должен быть меньше :value Килобайт(а).',
'numeric' => 'Значение поля ":attribute" должно быть меньше :value.',
'string' => 'Количество символов в поле ":attribute" должно быть меньше :value.',
],
'lte' => [
'array' => 'Количество элементов в поле ":attribute" должно быть :value или меньше.',
'file' => 'Размер файла в поле ":attribute" должен быть :value Килобайт(а) или меньше.',
'numeric' => 'Значение поля ":attribute" должно быть :value или меньше.',
'string' => 'Количество символов в поле ":attribute" должно быть :value или меньше.',
],
'mac_address' => 'Значение поля ":attribute" должно быть корректным MAC-адресом.',
'max' => [
'array' => 'Количество элементов в поле ":attribute" не может превышать :max.',
'file' => 'Размер файла в поле ":attribute" не может быть больше :max Килобайт(а).',
'numeric' => 'Значение поля ":attribute" не может быть больше :max.',
'string' => 'Количество символов в поле ":attribute" не может превышать :max.',
],
'mimes' => 'Файл в поле ":attribute" должен быть одного из следующих типов: :values.',
'mimetypes' => 'Файл в поле ":attribute" должен быть одного из следующих типов: :values.',
'min' => [
'array' => 'Количество элементов в поле ":attribute" должно быть не меньше :min.',
'file' => 'Размер файла в поле ":attribute" должен быть не меньше :min Килобайт(а).',
'numeric' => 'Значение поля ":attribute" должно быть не меньше :min.',
'string' => 'Количество символов в поле ":attribute" должно быть не меньше :min.',
],
'multiple_of' => 'Значение поля ":attribute" должно быть кратным :value',
'not_in' => 'Выбранное значение для ":attribute" некорректно.',
'not_regex' => 'Значение поля ":attribute" некорректно.',
'numeric' => 'Значение поля ":attribute" должно быть числом.',
'password' => 'Некорректный пароль.',
'present' => 'Значение поля ":attribute" должно присутствовать.',
'prohibited' => 'Значение поля ":attribute" запрещено.',
'prohibited_if' => 'Значение поля ":attribute" запрещено, когда :other равно :value.',
'prohibited_unless' => 'Значение поля ":attribute" запрещено, если :other не состоит в :values.',
'prohibits' => 'Значение поля ":attribute" запрещает присутствие :other.',
'regex' => 'Значение поля ":attribute" некорректно.',
'required' => 'Поле ":attribute" обязательно для заполнения.',
'required_array_keys' => 'Массив в поле ":attribute" обязательно должен иметь ключи: :values',
'required_if' => 'Поле ":attribute" обязательно для заполнения, когда ":other" равно :value.',
'required_unless' => 'Поле ":attribute" обязательно для заполнения, когда ":other" не равно :values.',
'required_with' => 'Поле ":attribute" обязательно для заполнения, когда :values указано.',
'required_with_all' => 'Поле ":attribute" обязательно для заполнения, когда :values указано.',
'required_without' => 'Поле ":attribute" обязательно для заполнения, когда :values не указано.',
'required_without_all' => 'Поле ":attribute" обязательно для заполнения, когда ни одно из :values не указано.',
'same' => 'Значения полей ":attribute" и ":other" должны совпадать.',
'size' => [
'array' => 'Количество элементов в поле ":attribute" должно быть равным :size.',
'file' => 'Размер файла в поле ":attribute" должен быть равен :size Килобайт(а).',
'numeric' => 'Значение поля ":attribute" должно быть равным :size.',
'string' => 'Количество символов в поле ":attribute" должно быть равным :size.',
],
'starts_with' => 'Поле ":attribute" должно начинаться с одного из следующих значений: :values',
'string' => 'Значение поля ":attribute" должно быть строкой.',
'timezone' => 'Значение поля ":attribute" должно быть действительным часовым поясом.',
'unique' => 'Такое значение поля ":attribute" уже существует.',
'uploaded' => 'Загрузка поля ":attribute" не удалась.',
'url' => 'Значение поля ":attribute" имеет ошибочный формат URL.',
'uuid' => 'Значение поля ":attribute" должно быть корректным UUID.',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'cron' => 'Значение поля ":attribute" не корректно.'
];

View File

@ -0,0 +1,212 @@
@props(['user'])
<div x-data="{
name: '{{ $user->name }}',
email: '{{ $user->email }}',
password: '{{ $user->password ?? null }}',
errors: {},
saveProfile() {
// Очищаем предыдущие ошибки
this.errors = {};
// Формируем объект данных только с заполненными полями
const data = {
_token: document.querySelector('meta[name=\'csrf-token\']').getAttribute('content')
};
// Отправляем только заполненные поля
if (this.name) {
data.name = this.name;
}
if (this.email) {
data.email = this.email;
}
if (this.password) {
data.password = this.password;
}
fetch('{{route('users.update', ['user_uuid' => $user->uuid])}}', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name=\'csrf-token\']').getAttribute('content')
},
body: JSON.stringify(data)
})
.then(response => {
// Выводим ответ сервера в консоль для диагностики
console.log('Response status:', response.status);
console.log('Response headers:', response.headers);
// Проверяем, является ли ответ редиректом
if (response.status === 302) {
const location = response.headers.get('location');
console.log('Redirect location:', location);
throw new Error('Server responded with redirect to: ' + location);
}
// Проверяем, является ли ответ JSON
const contentType = response.headers.get('content-type');
if (!contentType || !contentType.includes('application/json')) {
return response.text().then(text => {
console.log('Response text (non-JSON):', text);
throw new Error('Response is not JSON: ' + text);
});
}
if (!response.ok) {
// Обрабатываем ошибки валидации (статус 422)
if (response.status === 422) {
return response.json().then(errorData => {
console.log('Validation errors:', errorData);
// Сохраняем ошибки для отображения
this.errors = errorData.errors;
throw new Error('Validation failed');
});
}
return response.text().then(text => {
console.log('Error response text:', text);
throw new Error('Network response was not ok: ' + response.status + ' ' + text);
});
}
return response.json();
})
.then(data => {
if (data.success) {
// Очищаем ошибки при успешном обновлении
this.errors = {};
// Обновляем отображаемые данные
document.querySelector('h4').textContent = this.name;
// Закрываем модальное окно
this.open = false;
} else {
console.error('Error updating profile:', data.message);
}
})
.catch(error => {
console.error('Error:', error);
// Ошибки валидации уже сохранены в this.errors
if (error.message !== 'Validation failed') {
console.error('An error occurred while updating the profile:', error.message);
}
});
}
}">
<div class="mb-6 rounded-2xl border border-gray-200 p-5 lg:p-6 dark:border-gray-800">
<div class="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
<div class="flex w-full flex-col items-center gap-6 xl:flex-row">
<div class="h-20 w-20 overflow-hidden rounded-full border border-gray-200 dark:border-gray-800">
<img src="{{ $user->avatar ?? Vite::asset('resources/img/avatar.png') }}" alt="user" onerror="this.src='{{ Vite::asset('resources/img/avatar.png') }}'; this.onerror=null;" />
</div>
<div class="order-3 xl:order-2">
<h4 class="mb-2 text-center text-lg font-semibold text-gray-800 xl:text-left dark:text-white/90">
{{ $user->name }}
</h4>
<div class="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left">
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ $user->role->name }}
</p>
{{-- <div class="hidden h-3.5 w-px bg-gray-300 xl:block dark:bg-gray-700"></div>
<p class="text-sm text-gray-500 dark:text-gray-400">
Arizona, United States.
</p>--}}
</div>
</div>
</div>
<button @click="$dispatch('open-profile-info-modal')"
class="shadow-theme-xs flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-800 lg:inline-flex lg:w-auto dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200">
<svg class="fill-current" width="18" height="18" viewBox="0 0 18 18" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill="" />
</svg>
{{ __('Edit') }}
</button>
</div>
</div>
<!-- Profile Info Modal -->
<x-ui.modal x-data="{ open: false }" @open-profile-info-modal.window="open = true" :isOpen="false" class="max-w-[700px]">
<div
class="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 dark:bg-gray-900 lg:p-11">
<div class="px-2 pr-14">
<h4 class="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
{{ __('Edit') }}
</h4>
<p class="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
{{ __('Update your details to keep your profile up-to-date.') }}
</p>
</div>
<form class="flex flex-col">
<div class="custom-scrollbar h-[458px] overflow-y-auto p-2">
<div class="mt-7">
<h5 class="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
{{ __('Personal Information') }}
</h5>
<div class="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div class="col-span-2 lg:col-span-1">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
{{ __('Name') }}
</label>
<input type="text" x-model="name"
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
<template x-if="errors.name">
<div class="mt-1 text-sm text-red-500" x-text="errors.name[0]"></div>
</template>
</div>
<div class="col-span-2 lg:col-span-1">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
{{ __('Email Address') }}
</label>
<input type="text" x-model="email"
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
<template x-if="errors.email">
<div class="mt-1 text-sm text-red-500" x-text="errors.email[0]"></div>
</template>
</div>
<div class="col-span-2 lg:col-span-1">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
{{ __('Password') }}
</label>
<input type="password" x-model="password"
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
<template x-if="errors.password">
<div class="mt-1 text-sm text-red-500" x-text="errors.password[0]"></div>
</template>
</div>
@if(auth()->user()->role->code === 'admin')
<div class="col-span-2">
<label class="mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-400">
Bio
</label>
<input type="text" value="{{ $user->role->name }}"
class="dark:bg-dark-900 h-11 w-full appearance-none rounded-lg border border-gray-300 bg-transparent bg-none px-4 py-2.5 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800" />
</div>
@endif
</div>
</div>
</div>
<div class="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<button @click="open = false" type="button"
class="flex w-full justify-center rounded-lg border border-gray-300 bg-white px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] sm:w-auto">
Close
</button>
<button @click="saveProfile" type="button"
class="flex w-full justify-center rounded-lg bg-brand-500 px-4 py-2.5 text-sm font-medium text-white hover:bg-brand-600 sm:w-auto">
Save Changes
</button>
</div>
</form>
</div>
</x-ui.modal>
</div>

View File

@ -0,0 +1,59 @@
@props([
'isOpen' => false,
'showCloseButton' => true,
])
<div x-data="{
open: @js($isOpen),
init() {
this.$watch('open', value => {
if (value) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
});
}
}" x-show="open" x-cloak @keydown.escape.window="open = false"
class="modal fixed inset-0 z-99999 flex items-center justify-center overflow-y-auto p-5"
{{ $attributes->except('class') }}>
<!-- Backdrop -->
<div @click="open = false" class="fixed inset-0 h-full w-full bg-gray-400/50 backdrop-blur-[32px]"
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
</div>
<!-- Modal Content -->
<div @click.stop class="relative w-full rounded-3xl bg-white dark:bg-gray-900 {{ $attributes->get('class') }}"
x-transition:enter="transition ease-out duration-300" x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 transform scale-100"
x-transition:leave-end="opacity-0 transform scale-95">
<!-- Close Button -->
@if ($showCloseButton)
<button @click="open = false"
class="absolute right-3 top-3 z-999 flex h-9.5 w-9.5 items-center justify-center rounded-full bg-gray-100 text-gray-400 transition-colors hover:bg-gray-200 hover:text-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white sm:right-6 sm:top-6 sm:h-11 sm:w-11">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd"
d="M6.04289 16.5413C5.65237 16.9318 5.65237 17.565 6.04289 17.9555C6.43342 18.346 7.06658 18.346 7.45711 17.9555L11.9987 13.4139L16.5408 17.956C16.9313 18.3466 17.5645 18.3466 17.955 17.956C18.3455 17.5655 18.3455 16.9323 17.955 16.5418L13.4129 11.9997L17.955 7.4576C18.3455 7.06707 18.3455 6.43391 17.955 6.04338C17.5645 5.65286 16.9313 5.65286 16.5408 6.04338L11.9987 10.5855L7.45711 6.0439C7.06658 5.65338 6.43342 5.65338 6.04289 6.0439C5.65237 6.43442 5.65237 7.06759 6.04289 7.45811L10.5845 11.9997L6.04289 16.5413Z"
fill="currentColor" />
</svg>
</button>
@endif
<!-- Modal Body -->
<div>
{{ $slot }}
</div>
</div>
</div>
<style>
[x-cloak] {
display: none;
}
</style>

View File

@ -1,10 +1,10 @@
@extends ('layouts.app') @extends ('layouts.app')
@section ('content') @section ('content')
<x-common.page-breadcrumb pageTitle="User Profile" /> <x-common.page-breadcrumb pageTitle="{{ __('User Profile') }}" />
<div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6"> <div class="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] lg:p-6">
<h3 class="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">Profile</h3> <h3 class="mb-5 text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-7">{{ __('Profile') }}</h3>
<x-profile.profile-card /> <x-profile.profile-card :user="$data"/>
<x-profile.personal-info-card /> <x-profile.personal-info-card />
<x-profile.address-card /> <x-profile.address-card />
</div> </div>

View File

@ -23,6 +23,9 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/{user_uuid}', 'show') Route::get('/{user_uuid}', 'show')
->name('show') ->name('show')
->whereUuid('user_uuid'); ->whereUuid('user_uuid');
Route::patch('/{user_uuid}/update', 'update')
->name('update')
->whereUuid('user_uuid');
}); });
}); });