админка

This commit is contained in:
2026-03-13 23:33:35 +04:00
parent bae063a4fa
commit eaedeeb52b
41 changed files with 2506 additions and 67 deletions
@@ -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
+61
View File
@@ -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
+152
View File
@@ -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
+29 -1
View File
@@ -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>
+52
View File
@@ -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