Получение детальной информации о пользователе
This commit is contained in:
parent
1eaf2fa744
commit
324e57e5f5
@ -8,6 +8,7 @@ use Illuminate\Contracts\Support\Arrayable;
|
|||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
class AppException extends Exception implements Arrayable
|
class AppException extends Exception implements Arrayable
|
||||||
@ -19,8 +20,7 @@ class AppException extends Exception implements Arrayable
|
|||||||
string $message = "",
|
string $message = "",
|
||||||
int $code = 0,
|
int $code = 0,
|
||||||
Throwable|null $previous = null
|
Throwable|null $previous = null
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
$this->errorSlug = $errorSlug;
|
$this->errorSlug = $errorSlug;
|
||||||
parent::__construct($message, $code, $previous);
|
parent::__construct($message, $code, $previous);
|
||||||
}
|
}
|
||||||
@ -70,4 +70,16 @@ class AppException extends Exception implements Arrayable
|
|||||||
): never {
|
): never {
|
||||||
throw new self($slug, $message, $code, $previous);
|
throw new self($slug, $message, $code, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function serviceUnavailable(
|
||||||
|
string $message = 'Ошибка сервера',
|
||||||
|
Throwable|null $previous = null,
|
||||||
|
): never {
|
||||||
|
throw new self(
|
||||||
|
errorSlug: 'service_unavailable',
|
||||||
|
message: $message,
|
||||||
|
code: Response::HTTP_SERVICE_UNAVAILABLE,
|
||||||
|
previous: $previous
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
46
src/app/Domain/Shared/Repositories/BaseRepository.php
Normal file
46
src/app/Domain/Shared/Repositories/BaseRepository.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\Shared\Repositories;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Exceptions\AppException;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
abstract class BaseRepository extends Model
|
||||||
|
{
|
||||||
|
public string $model;
|
||||||
|
|
||||||
|
final public function __construct()
|
||||||
|
{
|
||||||
|
if (!isset($this->model)) {
|
||||||
|
AppException::serviceUnavailable('model должен быть определён в репозитории');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_subclass_of($this->model, Model::class)) {
|
||||||
|
AppException::serviceUnavailable('Модель репозитория должна быть подклассом Illuminate\Database\Eloquent\Model');
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __get($key)
|
||||||
|
{
|
||||||
|
AppException::serviceUnavailable('Невозможно получить свойство напрямую через репозиторий');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Подмена возвращаемого значения не репозиторий а модель
|
||||||
|
*
|
||||||
|
* @return Builder<static>
|
||||||
|
*/
|
||||||
|
public function newQueryWithoutScopes(): Builder
|
||||||
|
{
|
||||||
|
$calledClass = get_called_class();
|
||||||
|
$instance = new $calledClass();
|
||||||
|
$model = new $instance->model();
|
||||||
|
return $model->newQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
23
src/app/Domain/User/Actions/ShowAction.php
Normal file
23
src/app/Domain/User/Actions/ShowAction.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\User\Actions;
|
||||||
|
|
||||||
|
use App\Domain\User\Data\ShowRequest;
|
||||||
|
use App\Domain\User\Repositories\UserRepository;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class ShowAction
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(ShowRequest $request): User
|
||||||
|
{
|
||||||
|
return $this->userRepository->whereUuid($request->user_uuid)->firstOrFail();
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/app/Domain/User/Data/ShowRequest.php
Normal file
29
src/app/Domain/User/Data/ShowRequest.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\User\Data;
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Attributes\FromRouteParameter;
|
||||||
|
use Spatie\LaravelData\Attributes\MapName;
|
||||||
|
use Spatie\LaravelData\Attributes\Validation\Exists;
|
||||||
|
use Spatie\LaravelData\Attributes\Validation\StringType;
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||||
|
|
||||||
|
#[MapName(SnakeCaseMapper::class)]
|
||||||
|
class ShowRequest extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[StringType, FromRouteParameter('user_uuid'), Exists('users', 'uuid')]
|
||||||
|
public readonly string $user_uuid,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function attributes(...$args): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'user_uuid' => 'Идентификатор пользователя'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/app/Domain/User/Data/UpdateRequest.php
Normal file
14
src/app/Domain/User/Data/UpdateRequest.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Spatie\LaravelData\Data;
|
||||||
|
|
||||||
|
class UpdateRequest extends Data
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly string $user_uuid,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/app/Domain/User/Repositories/UserRepository.php
Normal file
13
src/app/Domain/User/Repositories/UserRepository.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Domain\User\Repositories;
|
||||||
|
|
||||||
|
use App\Domain\Shared\Repositories\BaseRepository;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class UserRepository extends BaseRepository
|
||||||
|
{
|
||||||
|
public string $model = User::class;
|
||||||
|
}
|
||||||
56
src/app/Helpers/MenuHelper.php
Normal file
56
src/app/Helpers/MenuHelper.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Helpers;
|
||||||
|
|
||||||
|
class MenuHelper
|
||||||
|
{
|
||||||
|
public static function getMainNavItems(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'icon' => 'dashboard',
|
||||||
|
'name' => 'Dashboard',
|
||||||
|
'path' => route('dashboard')
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'icon' => 'user-profile',
|
||||||
|
'name' => 'User Profile',
|
||||||
|
'path' => auth()->check() ? route('users.show', ['user_uuid' => auth()->user()->uuid]) : '#'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getOterItems(): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getMenuGroups(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
'title' => 'Menu',
|
||||||
|
'items' => self::getMainNavItems(),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'title' => 'Others',
|
||||||
|
'items' => self::getOterItems()
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isActive($path)
|
||||||
|
{
|
||||||
|
return request()->is($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getIconSvg($iconName)
|
||||||
|
{
|
||||||
|
$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>',
|
||||||
|
];
|
||||||
|
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>';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,10 +4,20 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Domain\User\Actions\ShowAction;
|
||||||
|
use App\Domain\User\Data\ShowRequest;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
public function index()
|
public function index(): View
|
||||||
{
|
{
|
||||||
return view('user.index', []);
|
return view('pages.user.index', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(ShowRequest $request, ShowAction $action): View
|
||||||
|
{
|
||||||
|
$data = $action->execute($request);
|
||||||
|
return view('pages.user.show', ['data' => $data]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Domain\Shared\Exceptions\AppException;
|
use App\Domain\Shared\Exceptions\AppException;
|
||||||
|
|||||||
@ -53,7 +53,7 @@ class FortifyServiceProvider extends ServiceProvider
|
|||||||
});
|
});
|
||||||
|
|
||||||
Fortify::loginView(function () {
|
Fortify::loginView(function () {
|
||||||
return view('auth.login');
|
return view('pages.auth.login');
|
||||||
});
|
});
|
||||||
|
|
||||||
Fortify::verifyEmailView(function () {
|
Fortify::verifyEmailView(function () {
|
||||||
|
|||||||
@ -9,7 +9,8 @@
|
|||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"laravel/fortify": "^1.34",
|
"laravel/fortify": "^1.34",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^2.10.1"
|
"laravel/tinker": "^2.10.1",
|
||||||
|
"spatie/laravel-data": "^4.19"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
1054
src/composer.lock
generated
1054
src/composer.lock
generated
File diff suppressed because it is too large
Load Diff
27
src/package-lock.json
generated
27
src/package-lock.json
generated
@ -4,6 +4,9 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"alpinejs": "^3.15.8"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
@ -1134,6 +1137,30 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vue/reactivity": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/shared": "3.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@vue/shared": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/alpinejs": {
|
||||||
|
"version": "3.15.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.8.tgz",
|
||||||
|
"integrity": "sha512-zxIfCRTBGvF1CCLIOMQOxAyBuqibxSEwS6Jm1a3HGA9rgrJVcjEWlwLcQTVGAWGS8YhAsTRLVrtQ5a5QT9bSSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@vue/reactivity": "~3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
|||||||
@ -13,5 +13,8 @@
|
|||||||
"laravel-vite-plugin": "^2.0.0",
|
"laravel-vite-plugin": "^2.0.0",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"vite": "^7.0.7"
|
"vite": "^7.0.7"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"alpinejs": "^3.15.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,15 @@
|
|||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"declare_strict_types": true,
|
"declare_strict_types": true,
|
||||||
"concat_space": {"spacing": "one"}
|
"concat_space": {"spacing": "one"},
|
||||||
|
"braces_position": {
|
||||||
|
"control_structures_opening_brace": "same_line",
|
||||||
|
"functions_opening_brace": "next_line_unless_newline_at_signature_end",
|
||||||
|
"anonymous_functions_opening_brace": "same_line",
|
||||||
|
"classes_opening_brace": "next_line_unless_newline_at_signature_end",
|
||||||
|
"anonymous_classes_opening_brace": "next_line_unless_newline_at_signature_end",
|
||||||
|
"allow_single_line_empty_anonymous_classes": false,
|
||||||
|
"allow_single_line_anonymous_functions": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 15 KiB |
@ -6,6 +6,282 @@
|
|||||||
@source '../**/*.js';
|
@source '../**/*.js';
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
--font-*: initial;
|
||||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
--font-outfit: Outfit, sans-serif;
|
||||||
|
|
||||||
|
--breakpoint-*: initial;
|
||||||
|
--breakpoint-2xsm: 375px;
|
||||||
|
--breakpoint-xsm: 425px;
|
||||||
|
--breakpoint-3xl: 2000px;
|
||||||
|
--breakpoint-sm: 640px;
|
||||||
|
--breakpoint-md: 768px;
|
||||||
|
--breakpoint-lg: 1024px;
|
||||||
|
--breakpoint-xl: 1280px;
|
||||||
|
--breakpoint-2xl: 1536px;
|
||||||
|
|
||||||
|
--text-title-2xl: 72px;
|
||||||
|
--text-title-2xl--line-height: 90px;
|
||||||
|
--text-title-xl: 60px;
|
||||||
|
--text-title-xl--line-height: 72px;
|
||||||
|
--text-title-lg: 48px;
|
||||||
|
--text-title-lg--line-height: 60px;
|
||||||
|
--text-title-md: 36px;
|
||||||
|
--text-title-md--line-height: 44px;
|
||||||
|
--text-title-sm: 30px;
|
||||||
|
--text-title-sm--line-height: 38px;
|
||||||
|
--text-theme-xl: 20px;
|
||||||
|
--text-theme-xl--line-height: 30px;
|
||||||
|
--text-theme-sm: 14px;
|
||||||
|
--text-theme-sm--line-height: 20px;
|
||||||
|
--text-theme-xs: 12px;
|
||||||
|
--text-theme-xs--line-height: 18px;
|
||||||
|
|
||||||
|
--color-current: currentColor;
|
||||||
|
--color-transparent: transparent;
|
||||||
|
--color-white: #ffffff;
|
||||||
|
--color-black: #101828;
|
||||||
|
|
||||||
|
--color-brand-25: #f2f7ff;
|
||||||
|
--color-brand-50: #ecf3ff;
|
||||||
|
--color-brand-100: #dde9ff;
|
||||||
|
--color-brand-200: #c2d6ff;
|
||||||
|
--color-brand-300: #9cb9ff;
|
||||||
|
--color-brand-400: #7592ff;
|
||||||
|
--color-brand-500: #465fff;
|
||||||
|
--color-brand-600: #3641f5;
|
||||||
|
--color-brand-700: #2a31d8;
|
||||||
|
--color-brand-800: #252dae;
|
||||||
|
--color-brand-900: #262e89;
|
||||||
|
--color-brand-950: #161950;
|
||||||
|
|
||||||
|
--color-blue-light-25: #f5fbff;
|
||||||
|
--color-blue-light-50: #f0f9ff;
|
||||||
|
--color-blue-light-100: #e0f2fe;
|
||||||
|
--color-blue-light-200: #b9e6fe;
|
||||||
|
--color-blue-light-300: #7cd4fd;
|
||||||
|
--color-blue-light-400: #36bffa;
|
||||||
|
--color-blue-light-500: #0ba5ec;
|
||||||
|
--color-blue-light-600: #0086c9;
|
||||||
|
--color-blue-light-700: #026aa2;
|
||||||
|
--color-blue-light-800: #065986;
|
||||||
|
--color-blue-light-900: #0b4a6f;
|
||||||
|
--color-blue-light-950: #062c41;
|
||||||
|
|
||||||
|
--color-gray-25: #fcfcfd;
|
||||||
|
--color-gray-50: #f9fafb;
|
||||||
|
--color-gray-100: #f2f4f7;
|
||||||
|
--color-gray-200: #e4e7ec;
|
||||||
|
--color-gray-300: #d0d5dd;
|
||||||
|
--color-gray-400: #98a2b3;
|
||||||
|
--color-gray-500: #667085;
|
||||||
|
--color-gray-600: #475467;
|
||||||
|
--color-gray-700: #344054;
|
||||||
|
--color-gray-800: #1d2939;
|
||||||
|
--color-gray-900: #101828;
|
||||||
|
--color-gray-950: #0c111d;
|
||||||
|
--color-gray-dark: #1a2231;
|
||||||
|
|
||||||
|
--color-orange-25: #fffaf5;
|
||||||
|
--color-orange-50: #fff6ed;
|
||||||
|
--color-orange-100: #ffead5;
|
||||||
|
--color-orange-200: #fddcab;
|
||||||
|
--color-orange-300: #feb273;
|
||||||
|
--color-orange-400: #fd853a;
|
||||||
|
--color-orange-500: #fb6514;
|
||||||
|
--color-orange-600: #ec4a0a;
|
||||||
|
--color-orange-700: #c4320a;
|
||||||
|
--color-orange-800: #9c2a10;
|
||||||
|
--color-orange-900: #7e2410;
|
||||||
|
--color-orange-950: #511c10;
|
||||||
|
|
||||||
|
--color-success-25: #f6fef9;
|
||||||
|
--color-success-50: #ecfdf3;
|
||||||
|
--color-success-100: #d1fadf;
|
||||||
|
--color-success-200: #a6f4c5;
|
||||||
|
--color-success-300: #6ce9a6;
|
||||||
|
--color-success-400: #32d583;
|
||||||
|
--color-success-500: #12b76a;
|
||||||
|
--color-success-600: #039855;
|
||||||
|
--color-success-700: #027a48;
|
||||||
|
--color-success-800: #05603a;
|
||||||
|
--color-success-900: #054f31;
|
||||||
|
--color-success-950: #053321;
|
||||||
|
|
||||||
|
--color-error-25: #fffbfa;
|
||||||
|
--color-error-50: #fef3f2;
|
||||||
|
--color-error-100: #fee4e2;
|
||||||
|
--color-error-200: #fecdca;
|
||||||
|
--color-error-300: #fda29b;
|
||||||
|
--color-error-400: #f97066;
|
||||||
|
--color-error-500: #f04438;
|
||||||
|
--color-error-600: #d92d20;
|
||||||
|
--color-error-700: #b42318;
|
||||||
|
--color-error-800: #912018;
|
||||||
|
--color-error-900: #7a271a;
|
||||||
|
--color-error-950: #55160c;
|
||||||
|
|
||||||
|
--color-warning-25: #fffcf5;
|
||||||
|
--color-warning-50: #fffaeb;
|
||||||
|
--color-warning-100: #fef0c7;
|
||||||
|
--color-warning-200: #fedf89;
|
||||||
|
--color-warning-300: #fec84b;
|
||||||
|
--color-warning-400: #fdb022;
|
||||||
|
--color-warning-500: #f79009;
|
||||||
|
--color-warning-600: #dc6803;
|
||||||
|
--color-warning-700: #b54708;
|
||||||
|
--color-warning-800: #93370d;
|
||||||
|
--color-warning-900: #7a2e0e;
|
||||||
|
--color-warning-950: #4e1d09;
|
||||||
|
|
||||||
|
--color-theme-pink-500: #ee46bc;
|
||||||
|
|
||||||
|
--color-theme-purple-500: #7a5af8;
|
||||||
|
|
||||||
|
--shadow-theme-md: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
|
||||||
|
--shadow-theme-lg: 0px 12px 16px -4px rgba(16, 24, 40, 0.08),
|
||||||
|
0px 4px 6px -2px rgba(16, 24, 40, 0.03);
|
||||||
|
--shadow-theme-sm: 0px 1px 3px 0px rgba(16, 24, 40, 0.1), 0px 1px 2px 0px rgba(16, 24, 40, 0.06);
|
||||||
|
--shadow-theme-xs: 0px 1px 2px 0px rgba(16, 24, 40, 0.05);
|
||||||
|
--shadow-theme-xl: 0px 20px 24px -4px rgba(16, 24, 40, 0.08),
|
||||||
|
0px 8px 8px -4px rgba(16, 24, 40, 0.03);
|
||||||
|
--shadow-datepicker: -5px 0 0 #262d3c, 5px 0 0 #262d3c;
|
||||||
|
--shadow-focus-ring: 0px 0px 0px 4px rgba(70, 95, 255, 0.12);
|
||||||
|
--shadow-slider-navigation: 0px 1px 2px 0px rgba(16, 24, 40, 0.1),
|
||||||
|
0px 1px 3px 0px rgba(16, 24, 40, 0.1);
|
||||||
|
--shadow-tooltip: 0px 4px 6px -2px rgba(16, 24, 40, 0.05),
|
||||||
|
-8px 0px 20px 8px rgba(16, 24, 40, 0.05);
|
||||||
|
|
||||||
|
--drop-shadow-4xl: 0 35px 35px rgba(0, 0, 0, 0.25), 0 45px 65px rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
|
--z-index-1: 1;
|
||||||
|
--z-index-9: 9;
|
||||||
|
--z-index-99: 99;
|
||||||
|
--z-index-999: 999;
|
||||||
|
--z-index-9999: 9999;
|
||||||
|
--z-index-99999: 99999;
|
||||||
|
--z-index-999999: 999999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-gray-200, currentColor);
|
||||||
|
}
|
||||||
|
button:not(:disabled),
|
||||||
|
[role='button']:not(:disabled) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply relative font-normal font-outfit z-1 bg-gray-50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item {
|
||||||
|
@apply relative flex items-center w-full gap-3 px-3 py-2 font-medium rounded-lg text-theme-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-active {
|
||||||
|
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-inactive {
|
||||||
|
@apply text-gray-700 hover:bg-gray-100 group-hover:text-gray-700 dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-icon {
|
||||||
|
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-icon-active {
|
||||||
|
@apply text-brand-500 dark:text-brand-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-icon-inactive {
|
||||||
|
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-arrow {
|
||||||
|
@apply relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-arrow-active {
|
||||||
|
@apply rotate-180 text-brand-500 dark:text-brand-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-item-arrow-inactive {
|
||||||
|
@apply text-gray-500 group-hover:text-gray-700 dark:text-gray-400 dark:group-hover:text-gray-300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-item {
|
||||||
|
@apply relative flex items-center gap-3 rounded-lg px-3 py-2.5 text-theme-sm font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-item-active {
|
||||||
|
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-item-inactive {
|
||||||
|
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-item {
|
||||||
|
@apply text-theme-sm relative flex items-center gap-3 rounded-lg px-3 py-2.5 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-item-active {
|
||||||
|
@apply bg-brand-50 text-brand-500 dark:bg-brand-500/[0.12] dark:text-brand-400;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-item-inactive {
|
||||||
|
@apply text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-white/5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-badge {
|
||||||
|
@apply text-success-600 dark:text-success-500 block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-badge-active {
|
||||||
|
@apply bg-success-100 dark:bg-success-500/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-badge-inactive {
|
||||||
|
@apply bg-success-50 group-hover:bg-success-100 dark:bg-success-500/15 dark:group-hover:bg-success-500/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-badge-pro {
|
||||||
|
@apply text-brand-600 dark:text-brand-500 block rounded-full px-2.5 py-0.5 text-xs font-medium uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-badge-pro-active {
|
||||||
|
@apply bg-brand-100 dark:bg-brand-500/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility menu-dropdown-badge-pro-inactive {
|
||||||
|
@apply bg-brand-50 group-hover:bg-brand-100 dark:bg-brand-500/15 dark:group-hover:bg-brand-500/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility no-scrollbar {
|
||||||
|
/* Chrome, Safari and Opera */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
-ms-overflow-style: none; /* IE and Edge */
|
||||||
|
scrollbar-width: none; /* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
@utility custom-scrollbar {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
@apply size-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
@apply rounded-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
@apply bg-gray-200 rounded-full dark:bg-gray-700;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
src/resources/img/avatar.png
Normal file
BIN
src/resources/img/avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
BIN
src/resources/img/avatar2.png
Normal file
BIN
src/resources/img/avatar2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
BIN
src/resources/img/logo.png
Normal file
BIN
src/resources/img/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@ -1 +1,11 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
|
import Alpine from 'alpinejs';
|
||||||
|
|
||||||
|
window.Alpine = Alpine;
|
||||||
|
|
||||||
|
Alpine.start();
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// console.log($store.isExpande)
|
||||||
|
});
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
@props(['pageTitle' => 'Page'])
|
||||||
@ -3,8 +3,21 @@
|
|||||||
<head>
|
<head>
|
||||||
@include('layouts.partials.head')
|
@include('layouts.partials.head')
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body
|
||||||
|
x-data="{ 'loaded': true}"
|
||||||
|
x-init="$store.sidebar.isExpanded = window.innerWidth >= 1280;
|
||||||
|
const checkMobile = () => {
|
||||||
|
if (window.innerWidth < 1280) {
|
||||||
|
$store.sidebar.setMobileOpen(false);
|
||||||
|
$store.sidebar.isExpanded = false;
|
||||||
|
} else {
|
||||||
|
$store.sidebar.isMobileOpen = false;
|
||||||
|
$store.sidebar.isExpanded = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', checkMobile);">
|
||||||
<div class="min-h-screen xl:flex">
|
<div class="min-h-screen xl:flex">
|
||||||
|
@include('layouts.partials.sidebar')
|
||||||
<div class="flex-1 transition-all duration-300 ease-in-out">
|
<div class="flex-1 transition-all duration-300 ease-in-out">
|
||||||
@include('layouts.partials.app-header')
|
@include('layouts.partials.app-header')
|
||||||
<div class="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">
|
<div class="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">
|
||||||
|
|||||||
@ -1,11 +1,108 @@
|
|||||||
<header class="flex items-center justify-between px-4 py-3">
|
<header
|
||||||
<nav class="flex-1">
|
class="sticky top-0 flex w-full bg-white border-gray-200 z-99999 dark:border-gray-800 dark:bg-gray-900 xl:border-b"
|
||||||
<ul class="flex justify-center space-x-4">
|
x-data="{
|
||||||
<li><a href="{{ route('user.index') }}">Пользователи</a></li>
|
isApplicationMenuOpen: false,
|
||||||
</ul>
|
toggleApplicationMenu() {
|
||||||
</nav>
|
this.isApplicationMenuOpen = !this.isApplicationMenuOpen;
|
||||||
<form method="POST" action="{{ route('logout')}}">
|
}
|
||||||
@csrf
|
}">
|
||||||
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Выйти</button>
|
<div class="flex flex-col items-center justify-between grow xl:flex-row xl:px-6">
|
||||||
</form>
|
<div
|
||||||
|
class="flex items-center justify-between w-full gap-2 px-3 py-3 border-b border-gray-200 dark:border-gray-800 sm:gap-4 xl:justify-normal xl:border-b-0 xl:px-0 lg:py-4">
|
||||||
|
|
||||||
|
<!-- Desktop Sidebar Toggle Button (visible on xl and up) -->
|
||||||
|
<button
|
||||||
|
class="hidden xl:flex items-center justify-center w-10 h-10 text-gray-500 border border-gray-200 rounded-lg dark:border-gray-800 dark:text-gray-400 lg:h-11 lg:w-11"
|
||||||
|
:class="{ 'bg-gray-100 dark:bg-white/[0.03]': !$store.sidebar.isExpanded }"
|
||||||
|
@click="$store.sidebar.toggleExpanded()" aria-label="Toggle Sidebar">
|
||||||
|
<svg x-show="!$store.sidebar.isMobileOpen" width="16" height="12" viewBox="0 0 16 12" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
<svg x-show="$store.sidebar.isMobileOpen" class="fill-current" 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="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
|
||||||
|
fill="" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Mobile Menu Toggle Button (visible below xl) -->
|
||||||
|
<button
|
||||||
|
class="flex xl:hidden items-center justify-center w-10 h-10 text-gray-500 rounded-lg dark:text-gray-400 lg:h-11 lg:w-11"
|
||||||
|
:class="{ 'bg-gray-100 dark:bg-white/[0.03]': $store.sidebar.isMobileOpen }"
|
||||||
|
@click="$store.sidebar.toggleMobileOpen()" aria-label="Toggle Mobile Menu">
|
||||||
|
<svg x-show="!$store.sidebar.isMobileOpen" width="16" height="12" viewBox="0 0 16 12" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M0.583252 1C0.583252 0.585788 0.919038 0.25 1.33325 0.25H14.6666C15.0808 0.25 15.4166 0.585786 15.4166 1C15.4166 1.41421 15.0808 1.75 14.6666 1.75L1.33325 1.75C0.919038 1.75 0.583252 1.41422 0.583252 1ZM0.583252 11C0.583252 10.5858 0.919038 10.25 1.33325 10.25L14.6666 10.25C15.0808 10.25 15.4166 10.5858 15.4166 11C15.4166 11.4142 15.0808 11.75 14.6666 11.75L1.33325 11.75C0.919038 11.75 0.583252 11.4142 0.583252 11ZM1.33325 5.25C0.919038 5.25 0.583252 5.58579 0.583252 6C0.583252 6.41421 0.919038 6.75 1.33325 6.75L7.99992 6.75C8.41413 6.75 8.74992 6.41421 8.74992 6C8.74992 5.58579 8.41413 5.25 7.99992 5.25L1.33325 5.25Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
</svg>
|
||||||
|
<svg x-show="$store.sidebar.isMobileOpen" class="fill-current" 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="M6.21967 7.28131C5.92678 6.98841 5.92678 6.51354 6.21967 6.22065C6.51256 5.92775 6.98744 5.92775 7.28033 6.22065L11.999 10.9393L16.7176 6.22078C17.0105 5.92789 17.4854 5.92788 17.7782 6.22078C18.0711 6.51367 18.0711 6.98855 17.7782 7.28144L13.0597 12L17.7782 16.7186C18.0711 17.0115 18.0711 17.4863 17.7782 17.7792C17.4854 18.0721 17.0105 18.0721 16.7176 17.7792L11.999 13.0607L7.28033 17.7794C6.98744 18.0722 6.51256 18.0722 6.21967 17.7794C5.92678 17.4865 5.92678 17.0116 6.21967 16.7187L10.9384 12L6.21967 7.28131Z"
|
||||||
|
fill="" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Logo (mobile only) -->
|
||||||
|
<a href="/" class="xl:hidden">
|
||||||
|
<img class="dark:hidden" src="{{ Vite::asset('resources/img/logo.png') }}" alt="Logo" width="32" height="32" />
|
||||||
|
<img class="hidden dark:block" src="{{ Vite::asset('resources/img/logo.png') }}" alt="Logo" width="32" height="32" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Application Menu Toggle (mobile only) -->
|
||||||
|
<button @click="toggleApplicationMenu()"
|
||||||
|
class="flex items-center justify-center w-10 h-10 text-gray-700 rounded-lg z-99999 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800 xl:hidden">
|
||||||
|
<!-- Dots Icon -->
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M5.99902 10.4951C6.82745 10.4951 7.49902 11.1667 7.49902 11.9951V12.0051C7.49902 12.8335 6.82745 13.5051 5.99902 13.5051C5.1706 13.5051 4.49902 12.8335 4.49902 12.0051V11.9951C4.49902 11.1667 5.1706 10.4951 5.99902 10.4951ZM17.999 10.4951C18.8275 10.4951 19.499 11.1667 19.499 11.9951V12.0051C19.499 12.8335 18.8275 13.5051 17.999 13.5051C17.1706 13.5051 16.499 12.8335 16.499 12.0051V11.9951C16.499 11.1667 17.1706 10.4951 17.999 10.4951ZM13.499 11.9951C13.499 11.1667 12.8275 10.4951 11.999 10.4951C11.1706 10.4951 10.499 11.1667 10.499 11.9951V12.0051C10.499 12.8335 11.1706 13.5051 11.999 13.5051C12.8275 13.5051 13.499 12.8335 13.499 12.0051V11.9951Z"
|
||||||
|
fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Search Bar (desktop only) -->
|
||||||
|
<div class="hidden xl:block">
|
||||||
|
<form>
|
||||||
|
<div class="relative">
|
||||||
|
<span class="absolute -translate-y-1/2 pointer-events-none left-4 top-1/2">
|
||||||
|
<!-- Search Icon -->
|
||||||
|
<svg class="fill-gray-500 dark:fill-gray-400" width="20" height="20"
|
||||||
|
viewBox="0 0 20 20" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||||
|
d="M3.04175 9.37363C3.04175 5.87693 5.87711 3.04199 9.37508 3.04199C12.8731 3.04199 15.7084 5.87693 15.7084 9.37363C15.7084 12.8703 12.8731 15.7053 9.37508 15.7053C5.87711 15.7053 3.04175 12.8703 3.04175 9.37363ZM9.37508 1.54199C5.04902 1.54199 1.54175 5.04817 1.54175 9.37363C1.54175 13.6991 5.04902 17.2053 9.37508 17.2053C11.2674 17.2053 13.003 16.5344 14.357 15.4176L17.177 18.238C17.4699 18.5309 17.9448 18.5309 18.2377 18.238C18.5306 17.9451 18.5306 17.4703 18.2377 17.1774L15.418 14.3573C16.5365 13.0033 17.2084 11.2669 17.2084 9.37363C17.2084 5.04817 13.7011 1.54199 9.37508 1.54199Z"
|
||||||
|
fill="" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<input type="text" placeholder="Search or type command..."
|
||||||
|
class="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 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-800 dark:bg-white/3 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]" />
|
||||||
|
<button
|
||||||
|
class="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
|
||||||
|
<span> ⌘ </span>
|
||||||
|
<span> K </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="isApplicationMenuOpen ? 'flex' : 'hidden'"
|
||||||
|
class="items-center justify-between w-full gap-4 px-5 py-4 xl:flex shadow-theme-md xl:justify-end xl:px-0 xl:shadow-none">
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="flex-1">
|
||||||
|
<ul class="flex justify-center space-x-4">
|
||||||
|
<li><a href="{{ route('users.index') }}">Пользователи</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<form method="POST" action="{{ route('logout')}}">
|
||||||
|
@csrf
|
||||||
|
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Выйти</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -4,3 +4,35 @@
|
|||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||||
<title>{{ config('app.name', 'Laravel') }} | @yield('title')</title>
|
<title>{{ config('app.name', 'Laravel') }} | @yield('title')</title>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.store('sidebar', {
|
||||||
|
// Initialize based on screen size
|
||||||
|
isExpanded: window.innerWidth >= 1280, // true for desktop, false for mobile
|
||||||
|
isMobileOpen: false,
|
||||||
|
isHovered: false,
|
||||||
|
|
||||||
|
toggleExpanded() {
|
||||||
|
this.isExpanded = !this.isExpanded;
|
||||||
|
// When toggling desktop sidebar, ensure mobile menu is closed
|
||||||
|
this.isMobileOpen = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleMobileOpen() {
|
||||||
|
this.isMobileOpen = !this.isMobileOpen;
|
||||||
|
// Don't modify isExpanded when toggling mobile menu
|
||||||
|
},
|
||||||
|
|
||||||
|
setMobileOpen(val) {
|
||||||
|
this.isMobileOpen = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
setHovered(val) {
|
||||||
|
// Only allow hover effects on desktop when sidebar is collapsed
|
||||||
|
if (window.innerWidth >= 1280 && !this.isExpanded) {
|
||||||
|
this.isHovered = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
229
src/resources/views/layouts/partials/sidebar.blade.php
Normal file
229
src/resources/views/layouts/partials/sidebar.blade.php
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
|
||||||
|
@php
|
||||||
|
use App\Helpers\MenuHelper;
|
||||||
|
$menuGroups = MenuHelper::getMenuGroups();
|
||||||
|
|
||||||
|
// Get current path
|
||||||
|
$currentPath = request()->path();
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<aside id="sidebar"
|
||||||
|
class="fixed flex flex-col mt-19 top-0 px-5 left-0 bg-white dark:bg-gray-900 dark:border-gray-800 text-gray-900 h-screen transition-all duration-300 ease-in-out z-99999 border-r border-gray-200 p-t-80"
|
||||||
|
x-data="{
|
||||||
|
openSubmenus: {},
|
||||||
|
init() {
|
||||||
|
// Auto-open Dashboard menu on page load
|
||||||
|
this.initializeActiveMenus();
|
||||||
|
},
|
||||||
|
initializeActiveMenus() {
|
||||||
|
const currentPath = '{{ $currentPath }}';
|
||||||
|
|
||||||
|
@foreach ($menuGroups as $groupIndex => $menuGroup)
|
||||||
|
@foreach ($menuGroup['items'] as $itemIndex => $item)
|
||||||
|
@if (isset($item['subItems']))
|
||||||
|
// Check if any submenu item matches current path
|
||||||
|
@foreach ($item['subItems'] as $subItem)
|
||||||
|
if (currentPath === '{{ ltrim($subItem['path'], '/') }}' ||
|
||||||
|
window.location.pathname === '{{ $subItem['path'] }}') {
|
||||||
|
this.openSubmenus['{{ $groupIndex }}-{{ $itemIndex }}'] = true;
|
||||||
|
} @endforeach
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
@endforeach
|
||||||
|
},
|
||||||
|
toggleSubmenu(groupIndex, itemIndex) {
|
||||||
|
const key = groupIndex + '-' + itemIndex;
|
||||||
|
const newState = !this.openSubmenus[key];
|
||||||
|
|
||||||
|
// Close all other submenus when opening a new one
|
||||||
|
if (newState) {
|
||||||
|
this.openSubmenus = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openSubmenus[key] = newState;
|
||||||
|
},
|
||||||
|
isSubmenuOpen(groupIndex, itemIndex) {
|
||||||
|
const key = groupIndex + '-' + itemIndex;
|
||||||
|
return this.openSubmenus[key] || false;
|
||||||
|
},
|
||||||
|
isActive(path) {
|
||||||
|
return window.location.pathname === path || '{{ $currentPath }}' === path.replace(/^\//, '');
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
:class="{
|
||||||
|
'w-[290px]': $store.sidebar.isExpanded || $store.sidebar.isMobileOpen || $store.sidebar.isHovered,
|
||||||
|
'w-[90px]': !$store.sidebar.isExpanded && !$store.sidebar.isHovered,
|
||||||
|
'translate-x-0': $store.sidebar.isMobileOpen,
|
||||||
|
'-translate-x-full xl:translate-x-0': !$store.sidebar.isMobileOpen
|
||||||
|
}"
|
||||||
|
@mouseenter="if (!$store.sidebar.isExpanded) $store.sidebar.setHovered(true)"
|
||||||
|
@mouseleave="$store.sidebar.setHovered(false)">
|
||||||
|
<!-- Logo Section ??? -->
|
||||||
|
{{-- <div class="pt-8 pb-7 flex"
|
||||||
|
:class="(!$store.sidebar.isExpanded && !$store.sidebar.isHovered && !$store.sidebar.isMobileOpen) ?
|
||||||
|
'xl:justify-center' :
|
||||||
|
'justify-start'">
|
||||||
|
<a href="{{ route('dashboard') }}">
|
||||||
|
<img x-show="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen"
|
||||||
|
class="dark:hidden" src="{{ Vite::asset('resources/img/logo.png') }}" alt="Logo" width="150" height="40" />
|
||||||
|
<img x-show="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen"
|
||||||
|
class="hidden dark:block" src="{{ Vite::asset('resources/img/logo.png') }}" alt="Logo" width="150"
|
||||||
|
height="40" />
|
||||||
|
<img x-show="!$store.sidebar.isExpanded && !$store.sidebar.isHovered && !$store.sidebar.isMobileOpen"
|
||||||
|
src="{{ Vite::asset('resources/img/logo.png') }}" alt="Logo" width="32" height="32" />
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</div> --}}
|
||||||
|
|
||||||
|
<!-- Navigation Menu -->
|
||||||
|
<div class="flex flex-col overflow-y-auto duration-300 ease-linear no-scrollbar">
|
||||||
|
<nav class="mb-6">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
@foreach ($menuGroups as $groupIndex => $menuGroup)
|
||||||
|
<div>
|
||||||
|
<!-- Menu Group Title -->
|
||||||
|
<h2 class="mb-4 text-xs uppercase flex leading-[20px] text-gray-400"
|
||||||
|
:class="(!$store.sidebar.isExpanded && !$store.sidebar.isHovered && !$store.sidebar.isMobileOpen) ?
|
||||||
|
'lg:justify-center' : 'justify-start'">
|
||||||
|
<template
|
||||||
|
x-if="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen">
|
||||||
|
<span>{{ $menuGroup['title'] }}</span>
|
||||||
|
</template>
|
||||||
|
<template x-if="!$store.sidebar.isExpanded && !$store.sidebar.isHovered && !$store.sidebar.isMobileOpen">
|
||||||
|
<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.99915 10.2451C6.96564 10.2451 7.74915 11.0286 7.74915 11.9951V12.0051C7.74915 12.9716 6.96564 13.7551 5.99915 13.7551C5.03265 13.7551 4.24915 12.9716 4.24915 12.0051V11.9951C4.24915 11.0286 5.03265 10.2451 5.99915 10.2451ZM17.9991 10.2451C18.9656 10.2451 19.7491 11.0286 19.7491 11.9951V12.0051C19.7491 12.9716 18.9656 13.7551 17.9991 13.7551C17.0326 13.7551 16.2491 12.9716 16.2491 12.0051V11.9951C16.2491 11.0286 17.0326 10.2451 17.9991 10.2451ZM13.7491 11.9951C13.7491 11.0286 12.9656 10.2451 11.9991 10.2451C11.0326 10.2451 10.2491 11.0286 10.2491 11.9951V12.0051C10.2491 12.9716 11.0326 13.7551 11.9991 13.7551C12.9656 13.7551 13.7491 12.9716 13.7491 12.0051V11.9951Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Menu Items -->
|
||||||
|
<ul class="flex flex-col gap-1">
|
||||||
|
@foreach ($menuGroup['items'] as $itemIndex => $item)
|
||||||
|
<li>
|
||||||
|
@if (isset($item['subItems']))
|
||||||
|
<!-- Menu Item with Submenu -->
|
||||||
|
<button @click="toggleSubmenu({{ $groupIndex }}, {{ $itemIndex }})"
|
||||||
|
class="menu-item group w-full"
|
||||||
|
:class="[
|
||||||
|
isSubmenuOpen({{ $groupIndex }}, {{ $itemIndex }}) ?
|
||||||
|
'menu-item-active' : 'menu-item-inactive',
|
||||||
|
!$store.sidebar.isExpanded && !$store.sidebar.isHovered ?
|
||||||
|
'xl:justify-center' : 'xl:justify-start'
|
||||||
|
]">
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<span :class="isSubmenuOpen({{ $groupIndex }}, {{ $itemIndex }}) ?
|
||||||
|
'menu-item-icon-active' : 'menu-item-icon-inactive'">
|
||||||
|
{!! MenuHelper::getIconSvg($item['icon']) !!}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<span
|
||||||
|
x-show="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen"
|
||||||
|
class="menu-item-text flex items-center gap-2">
|
||||||
|
{{ $item['name'] }}
|
||||||
|
@if (!empty($item['new']))
|
||||||
|
<span class="absolute right-10"
|
||||||
|
:class="isActive('{{ $item['path'] ?? '' }}') ?
|
||||||
|
'menu-dropdown-badge menu-dropdown-badge-active' :
|
||||||
|
'menu-dropdown-badge menu-dropdown-badge-inactive'">
|
||||||
|
new
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Chevron Down Icon -->
|
||||||
|
<svg x-show="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen"
|
||||||
|
class="ml-auto w-5 h-5 transition-transform duration-200"
|
||||||
|
:class="{
|
||||||
|
'rotate-180 text-brand-500': isSubmenuOpen({{ $groupIndex }},
|
||||||
|
{{ $itemIndex }})
|
||||||
|
}"
|
||||||
|
fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Submenu -->
|
||||||
|
<div x-show="isSubmenuOpen({{ $groupIndex }}, {{ $itemIndex }}) && ($store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen)">
|
||||||
|
<ul class="mt-2 space-y-1 ml-9">
|
||||||
|
@foreach ($item['subItems'] as $subItem)
|
||||||
|
<li>
|
||||||
|
<a href="{{ $subItem['path'] }}" class="menu-dropdown-item"
|
||||||
|
:class="isActive('{{ $subItem['path'] }}') ?
|
||||||
|
'menu-dropdown-item-active' :
|
||||||
|
'menu-dropdown-item-inactive'">
|
||||||
|
{{ $subItem['name'] }}
|
||||||
|
<span class="flex items-center gap-1 ml-auto">
|
||||||
|
@if (!empty($subItem['new']))
|
||||||
|
<span
|
||||||
|
:class="isActive('{{ $subItem['path'] }}') ?
|
||||||
|
'menu-dropdown-badge menu-dropdown-badge-active' :
|
||||||
|
'menu-dropdown-badge menu-dropdown-badge-inactive'">
|
||||||
|
new
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
@if (!empty($subItem['pro']))
|
||||||
|
<span
|
||||||
|
:class="isActive('{{ $subItem['path'] }}') ?
|
||||||
|
'menu-dropdown-badge-pro menu-dropdown-badge-pro-active' :
|
||||||
|
'menu-dropdown-badge-pro menu-dropdown-badge-pro-inactive'">
|
||||||
|
pro
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<!-- Simple Menu Item -->
|
||||||
|
<a href="{{ $item['path'] }}" class="menu-item group"
|
||||||
|
:class="[
|
||||||
|
isActive('{{ $item['path'] }}') ? 'menu-item-active' :
|
||||||
|
'menu-item-inactive',
|
||||||
|
(!$store.sidebar.isExpanded && !$store.sidebar.isHovered && !$store.sidebar.isMobileOpen) ?
|
||||||
|
'xl:justify-center' :
|
||||||
|
'justify-start'
|
||||||
|
]">
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<span
|
||||||
|
:class="isActive('{{ $item['path'] }}') ? 'menu-item-icon-active' :
|
||||||
|
'menu-item-icon-inactive'">
|
||||||
|
{!! MenuHelper::getIconSvg($item['icon']) !!}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Text -->
|
||||||
|
<span
|
||||||
|
x-show="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen"
|
||||||
|
class="menu-item-text flex items-center gap-2">
|
||||||
|
{{ $item['name'] }}
|
||||||
|
@if (!empty($item['new']))
|
||||||
|
<span
|
||||||
|
class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold bg-brand-500 text-white">
|
||||||
|
new
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Sidebar Widget -->
|
||||||
|
<div x-data x-show="$store.sidebar.isExpanded || $store.sidebar.isHovered || $store.sidebar.isMobileOpen" x-transition class="mt-auto">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Mobile Overlay -->
|
||||||
|
<div x-show="$store.sidebar.isMobileOpen" @click="$store.sidebar.setMobileOpen(false)"
|
||||||
|
class="fixed z-50 h-screen w-full bg-gray-900/50"></div>
|
||||||
13
src/resources/views/pages/user/show.blade.php
Normal file
13
src/resources/views/pages/user/show.blade.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
@extends ('layouts.app')
|
||||||
|
|
||||||
|
@section ('content')
|
||||||
|
<h1>{{ $data }}</h1>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<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.personal-info-card />
|
||||||
|
<x-profile.address-card />
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@ -13,7 +13,16 @@ Route::get('/', function () {
|
|||||||
Route::middleware(['auth', 'verified'])->group(function () {
|
Route::middleware(['auth', 'verified'])->group(function () {
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])
|
Route::get('/dashboard', [DashboardController::class, 'index'])
|
||||||
->name('dashboard');
|
->name('dashboard');
|
||||||
Route::get('/users', [UserController::class, 'index'])
|
Route::controller(UserController::class)
|
||||||
->name('user.index')
|
->prefix('/users')
|
||||||
->middleware('role:admin');
|
->as('users.')
|
||||||
|
->group(static function (): void {
|
||||||
|
Route::get('/', 'index')
|
||||||
|
->name('index')
|
||||||
|
->middleware('role:admin');
|
||||||
|
Route::get('/{user_uuid}', 'show')
|
||||||
|
->name('show')
|
||||||
|
->whereUuid('user_uuid');
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
23
src/tests/Feature/UserControllerTest.php
Normal file
23
src/tests/Feature/UserControllerTest.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
use function Pest\Laravel\actingAs;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
it('получение профиля пользователя', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$response = actingAs($user)
|
||||||
|
->get(route('users.show', ['user_uuid' => $user->uuid]));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertViewIs('pages.user.show');
|
||||||
|
$response->assertViewHas('data', $user);
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user