админка
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
@extends('layouts.portal')
|
||||
|
||||
@section('portal-content')
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<span class="eyebrow">Инвайты</span>
|
||||
<h2>Управление инвайт-кодами</h2>
|
||||
<p>Без действующего инвайт-кода регистрация нового аккаунта недоступна.</p>
|
||||
</div>
|
||||
|
||||
<div class="portal-grid">
|
||||
<article class="content-card">
|
||||
<h3>Создать инвайт-коды</h3>
|
||||
|
||||
<dl class="stats-list stats-list--compact">
|
||||
<div>
|
||||
<dt>Всего</dt>
|
||||
<dd>{{ $inviteStats['total'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Доступно</dt>
|
||||
<dd>{{ $inviteStats['available'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Использовано</dt>
|
||||
<dd>{{ $inviteStats['redeemed'] }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<form class="inline-form" method="POST" action="{{ route('admin.invite-codes.store') }}">
|
||||
@csrf
|
||||
|
||||
<label class="form-field">
|
||||
<span>Количество</span>
|
||||
<input class="form-control @error('quantity') is-invalid @enderror" type="number" min="1" max="50" name="quantity" value="{{ old('quantity', 1) }}" required>
|
||||
@error('quantity')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<button class="button button--gold" type="submit">Создать коды</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="content-card">
|
||||
<h3>Последние инвайт-коды</h3>
|
||||
|
||||
@if ($inviteCodes->isNotEmpty())
|
||||
<div class="character-list">
|
||||
@foreach ($inviteCodes as $invite)
|
||||
<div class="character-row">
|
||||
<div>
|
||||
<strong>{{ $invite->code }}</strong>
|
||||
<span>
|
||||
Создал: {{ $invite->created_by_username ?: '—' }}
|
||||
@if ($invite->created_at)
|
||||
· {{ $invite->created_at->format('d.m.Y H:i') }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $invite->redeemed_at ? 'Использован' : 'Свободен' }}</strong>
|
||||
<span>
|
||||
{{ $invite->redeemed_by_username ?: 'ещё не использован' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-inline">Инвайт-коды ещё не создавались.</div>
|
||||
@endif
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<span class="eyebrow">Админка</span>
|
||||
<h2>Аккаунты и персонажи</h2>
|
||||
<p>Здесь видны все игровые аккаунты, их персонажи и текущий уровень доступа.</p>
|
||||
</div>
|
||||
|
||||
<form class="search-form" method="GET" action="{{ route('admin.accounts.index') }}">
|
||||
<input class="form-control" type="search" name="search" value="{{ $search }}" placeholder="Поиск по логину или email">
|
||||
<button class="button button--gold" type="submit">Найти</button>
|
||||
@if ($search !== '')
|
||||
<a class="button button--ghost" href="{{ route('admin.accounts.index') }}">Сбросить</a>
|
||||
@endif
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="account-list">
|
||||
@forelse ($accounts as $account)
|
||||
<article class="content-card account-card">
|
||||
<div class="account-card__header">
|
||||
<div>
|
||||
<h3>{{ $account['username'] }}</h3>
|
||||
<p>ID #{{ $account['id'] }} · {{ $account['email'] !== '' ? $account['email'] : 'email не указан' }}</p>
|
||||
</div>
|
||||
|
||||
<span class="tag">{{ $account['access_label'] }}</span>
|
||||
</div>
|
||||
|
||||
<dl class="stats-list stats-list--compact">
|
||||
<div>
|
||||
<dt>Регистрация</dt>
|
||||
<dd>{{ $account['joined_at'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Последний вход</dt>
|
||||
<dd>{{ $account['last_login_at'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Статус</dt>
|
||||
<dd>{{ $account['locked'] ? 'Заблокирован' : 'Активен' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Персонажей</dt>
|
||||
<dd>{{ $account['characters']->count() }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<form class="inline-form" method="POST" action="{{ route('admin.accounts.access.update', $account['id']) }}">
|
||||
@csrf
|
||||
@method('PATCH')
|
||||
|
||||
<label class="form-field">
|
||||
<span>Уровень доступа</span>
|
||||
<select class="form-control @error('gmlevel') is-invalid @enderror" name="gmlevel">
|
||||
@foreach ($accessLevels as $level => $label)
|
||||
<option value="{{ $level }}" @selected($account['gm_level'] === $level)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<button class="button button--gold" type="submit">Сохранить</button>
|
||||
</form>
|
||||
|
||||
@if ($account['characters']->isNotEmpty())
|
||||
<div class="character-list">
|
||||
@foreach ($account['characters'] as $character)
|
||||
<div class="character-row">
|
||||
<div>
|
||||
<strong>{{ $character['name'] }}</strong>
|
||||
<span>{{ $character['race'] }} · {{ $character['class'] }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>{{ $character['level'] }} ур.</strong>
|
||||
<span>{{ $character['online'] ? 'Онлайн' : 'Оффлайн' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-inline">На аккаунте пока нет персонажей.</div>
|
||||
@endif
|
||||
</article>
|
||||
@empty
|
||||
<article class="content-card empty-state">
|
||||
<h3>Аккаунты не найдены</h3>
|
||||
<p>Измени поисковый запрос или попробуй позже.</p>
|
||||
</article>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,61 @@
|
||||
@extends('layouts.portal')
|
||||
|
||||
@section('portal-content')
|
||||
<section class="section section--split">
|
||||
<article class="content-card">
|
||||
<span class="eyebrow">Личный кабинет</span>
|
||||
<h2>Вход в игровой аккаунт</h2>
|
||||
<p>
|
||||
Используй логин и пароль от игрового аккаунта. После входа будут доступны персонажи,
|
||||
смена пароля и ссылка на клиент игры.
|
||||
</p>
|
||||
|
||||
<ul class="feature-points">
|
||||
<li>Просмотр всех персонажей аккаунта</li>
|
||||
<li>Смена пароля без ручных команд</li>
|
||||
<li>Быстрый переход к клиенту игры</li>
|
||||
</ul>
|
||||
</article>
|
||||
|
||||
<aside class="hero-panel">
|
||||
<div class="panel-header">
|
||||
<span class="eyebrow">Авторизация</span>
|
||||
<h2>Войти</h2>
|
||||
<p>Вход выполняется по игровым учётным данным.</p>
|
||||
</div>
|
||||
|
||||
<form class="register-form" method="POST" action="{{ route('login.store') }}">
|
||||
@csrf
|
||||
|
||||
<label class="form-field">
|
||||
<span>Логин</span>
|
||||
<input
|
||||
class="form-control @error('username') is-invalid @enderror"
|
||||
type="text"
|
||||
name="username"
|
||||
value="{{ old('username') }}"
|
||||
autocomplete="username"
|
||||
maxlength="32"
|
||||
required
|
||||
>
|
||||
@error('username')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Пароль</span>
|
||||
<input
|
||||
class="form-control @error('password') is-invalid @enderror"
|
||||
type="password"
|
||||
name="password"
|
||||
autocomplete="current-password"
|
||||
maxlength="32"
|
||||
required
|
||||
>
|
||||
@error('password')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<button class="button button--gold button--full" type="submit">Войти в кабинет</button>
|
||||
</form>
|
||||
</aside>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -0,0 +1,152 @@
|
||||
@extends('layouts.portal')
|
||||
|
||||
@section('portal-content')
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<span class="eyebrow">Личный кабинет</span>
|
||||
<h2>{{ $user->username }}</h2>
|
||||
<p>Здесь можно посмотреть персонажей, сменить пароль и получить ссылку на клиент игры.</p>
|
||||
</div>
|
||||
|
||||
<div class="portal-grid">
|
||||
<article class="content-card">
|
||||
<h3>Данные аккаунта</h3>
|
||||
|
||||
<dl class="stats-list">
|
||||
<div>
|
||||
<dt>Логин</dt>
|
||||
<dd>{{ $user->username }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Email</dt>
|
||||
<dd>{{ $user->email !== '' ? $user->email : '—' }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Уровень доступа</dt>
|
||||
<dd>{{ $user->accessLabel() }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Регистрация</dt>
|
||||
<dd>{{ $user->joinedAtLabel() }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Последний вход</dt>
|
||||
<dd>{{ $user->lastLoginLabel() }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Статус</dt>
|
||||
<dd>{{ $user->locked ? 'Заблокирован' : 'Активен' }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
@if ($user->canAccessAdminPanel())
|
||||
<a class="button button--ghost" href="{{ route('admin.accounts.index') }}">Перейти в админку</a>
|
||||
@endif
|
||||
</article>
|
||||
|
||||
<article class="content-card">
|
||||
<h3>Клиент игры</h3>
|
||||
<p>Ссылка формируется при нажатии. Файл выдаётся напрямую из хранилища.</p>
|
||||
|
||||
<div class="action-stack">
|
||||
<a class="button button--gold" href="{{ route('cabinet.client') }}">Получить ссылку на клиент</a>
|
||||
<span class="muted-note">Файл: {{ $clientFilename }}</span>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section section--split">
|
||||
<article class="content-card">
|
||||
<span class="eyebrow">Безопасность</span>
|
||||
<h2>Сменить пароль</h2>
|
||||
<p>После обновления новый пароль сразу начнёт действовать для входа в игру и кабинет.</p>
|
||||
|
||||
<form class="register-form" method="POST" action="{{ route('cabinet.password.update') }}">
|
||||
@csrf
|
||||
|
||||
<label class="form-field">
|
||||
<span>Текущий пароль</span>
|
||||
<input class="form-control @error('current_password') is-invalid @enderror" type="password" name="current_password" maxlength="32" required>
|
||||
@error('current_password')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Новый пароль</span>
|
||||
<input class="form-control @error('password') is-invalid @enderror" type="password" name="password" maxlength="32" required>
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Подтверждение нового пароля</span>
|
||||
<input class="form-control @error('password') is-invalid @enderror" type="password" name="password_confirmation" maxlength="32" required>
|
||||
@error('password')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<button class="button button--gold button--full" type="submit">Обновить пароль</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="content-card">
|
||||
<span class="eyebrow">Персонажи</span>
|
||||
<h2>Краткая сводка</h2>
|
||||
<p>Всего персонажей: {{ $characters->count() }}. Ниже показан полный список по этому аккаунту.</p>
|
||||
|
||||
@if ($characters->isNotEmpty())
|
||||
<div class="tag-list">
|
||||
@foreach ($characters as $character)
|
||||
<span class="tag">{{ $character['name'] }} · {{ $character['class'] }} · {{ $character['level'] }}</span>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="empty-inline">Персонажей пока нет.</div>
|
||||
@endif
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<div class="section-heading">
|
||||
<span class="eyebrow">Мои персонажи</span>
|
||||
<h2>Все персонажи аккаунта</h2>
|
||||
<p>Здесь отображаются игровые персонажи, связанные с текущим аккаунтом.</p>
|
||||
</div>
|
||||
|
||||
<div class="character-grid">
|
||||
@forelse ($characters as $character)
|
||||
<article class="character-card">
|
||||
<div class="character-card__top">
|
||||
<div>
|
||||
<h3>{{ $character['name'] }}</h3>
|
||||
<p>{{ $character['race'] }} · {{ $character['class'] }}</p>
|
||||
</div>
|
||||
|
||||
<span class="tag">{{ $character['online'] ? 'Онлайн' : 'Оффлайн' }}</span>
|
||||
</div>
|
||||
|
||||
<dl class="stats-list stats-list--compact">
|
||||
<div>
|
||||
<dt>Уровень</dt>
|
||||
<dd>{{ $character['level'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Пол</dt>
|
||||
<dd>{{ $character['gender'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Золото</dt>
|
||||
<dd>{{ $character['money'] }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Наработано</dt>
|
||||
<dd>{{ $character['played'] }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
@empty
|
||||
<article class="content-card empty-state">
|
||||
<h3>Персонажей пока нет</h3>
|
||||
<p>Создай персонажа в игре, и он появится здесь автоматически.</p>
|
||||
</article>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
@endsection
|
||||
@@ -17,6 +17,18 @@
|
||||
<a href="#start">Как начать</a>
|
||||
<a href="#register">Регистрация</a>
|
||||
</nav>
|
||||
|
||||
<div class="topbar-actions">
|
||||
@auth
|
||||
<a class="button button--ghost" href="{{ route('cabinet.index') }}">Личный кабинет</a>
|
||||
|
||||
@if (auth()->user()->canAccessAdminPanel())
|
||||
<a class="button button--ghost" href="{{ route('admin.accounts.index') }}">Админка</a>
|
||||
@endif
|
||||
@else
|
||||
<a class="button button--ghost" href="{{ route('login') }}">Войти</a>
|
||||
@endauth
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@@ -49,7 +61,7 @@
|
||||
<div class="panel-header">
|
||||
<span class="eyebrow">Регистрация</span>
|
||||
<h2>Создать игровой аккаунт</h2>
|
||||
<p>Укажи логин, почту и пароль — аккаунт будет готов к входу в игру сразу после регистрации.</p>
|
||||
<p>Укажи инвайт-код, логин, почту и пароль — аккаунт будет готов к входу в игру сразу после регистрации.</p>
|
||||
</div>
|
||||
|
||||
@if (session('registration_success'))
|
||||
@@ -94,6 +106,22 @@
|
||||
@error('email')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<label class="form-field">
|
||||
<span>Инвайт-код</span>
|
||||
<input
|
||||
class="form-control @error('invite_code') is-invalid @enderror"
|
||||
type="text"
|
||||
name="invite_code"
|
||||
value="{{ old('invite_code') }}"
|
||||
autocomplete="off"
|
||||
placeholder="ABCD-EFGH-IJKL"
|
||||
maxlength="32"
|
||||
required
|
||||
>
|
||||
<small>Регистрация доступна только по действующему инвайт-коду.</small>
|
||||
@error('invite_code')<span class="form-error">{{ $message }}</span>@enderror
|
||||
</label>
|
||||
|
||||
<div class="form-row">
|
||||
<label class="form-field">
|
||||
<span>Пароль</span>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('content')
|
||||
@php($portalUser = auth()->user())
|
||||
|
||||
<div class="page-shell">
|
||||
<header class="topbar">
|
||||
<a class="brand" href="{{ route('landing.index') }}">
|
||||
<span class="brand-mark"></span>
|
||||
<span>
|
||||
<strong>{{ config('moonwell.realm.server_name') }}</strong>
|
||||
<small>{{ config('moonwell.realm.tagline') }}</small>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav class="topnav">
|
||||
<a href="{{ route('landing.index') }}">Главная</a>
|
||||
@if ($portalUser)
|
||||
<a href="{{ route('cabinet.index') }}">Личный кабинет</a>
|
||||
@if ($portalUser->canAccessAdminPanel())
|
||||
<a href="{{ route('admin.accounts.index') }}">Админка</a>
|
||||
@endif
|
||||
@endif
|
||||
</nav>
|
||||
|
||||
<div class="topbar-actions">
|
||||
@if ($portalUser)
|
||||
<span class="topbar-user">{{ $portalUser->username }} · {{ $portalUser->accessLabel() }}</span>
|
||||
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
<button class="button button--ghost" type="submit">Выйти</button>
|
||||
</form>
|
||||
@else
|
||||
<a class="button button--ghost" href="{{ route('login') }}">Войти</a>
|
||||
@endif
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (session('status'))
|
||||
<div class="alert alert--success">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
@if (session('error'))
|
||||
<div class="alert alert--error">{{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
<main>
|
||||
@yield('portal-content')
|
||||
</main>
|
||||
</div>
|
||||
@endsection
|
||||
Reference in New Issue
Block a user