новый дизайн

This commit is contained in:
2026-07-25 18:36:45 +04:00
parent df7f98fe8b
commit c43a73e148
54 changed files with 4234 additions and 1110 deletions
@@ -1,178 +0,0 @@
@extends('layouts.portal')
@section('portal-content')
<section class="section">
<div class="section-heading">
<span class="eyebrow">Инвайты</span>
<h2>Управление инвайт-кодами</h2>
<p>{{ config('moonwell.registration.require_invite_code') ? 'Регистрация требует действующий инвайт-код.' : 'Регистрация сейчас открыта без инвайт-кодов.' }}</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>
<div class="tab-switcher">
@foreach ($accountTabs as $tabKey => $tab)
<a
class="button {{ $activeTab === $tabKey ? 'button--gold' : 'button--ghost' }}"
href="{{ route('admin.accounts.index', array_filter(['tab' => $tabKey, 'search' => $search !== '' ? $search : null])) }}"
>
{{ $tab['label'] }} · {{ $tab['count'] }}
</a>
@endforeach
</div>
<form class="search-form" method="GET" action="{{ route('admin.accounts.index') }}">
<input type="hidden" name="tab" value="{{ $activeTab }}">
<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', ['tab' => $activeTab]) }}">Сбросить</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>{{ $activeTab === 'bots' ? 'Боты не найдены' : 'Игроки не найдены' }}</h3>
<p>Измени поисковый запрос или переключись на другую вкладку.</p>
</article>
@endforelse
</div>
</section>
@endsection
@@ -1,102 +0,0 @@
@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>
<form class="register-form" method="POST" action="{{ route('admin.balances.adjust') }}">
@csrf
<label class="form-field">
<span>ID или логин аккаунта</span>
<input class="form-control @error('account') is-invalid @enderror" name="account" value="{{ old('account') }}" required>
@error('account')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="form-field">
<span>Операция</span>
<select class="form-control @error('direction') is-invalid @enderror" name="direction" required>
<option value="credit" @selected(old('direction') === 'credit')>Начислить</option>
<option value="debit" @selected(old('direction') === 'debit')>Снять</option>
</select>
</label>
<label class="form-field">
<span>Количество Слёз Элуны</span>
<input class="form-control @error('amount') is-invalid @enderror" type="number" name="amount" min="0.01" max="1000000" step="0.01" value="{{ old('amount') }}" required>
@error('amount')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="form-field">
<span>Причина</span>
<input class="form-control @error('reason') is-invalid @enderror" name="reason" maxlength="255" value="{{ old('reason') }}" placeholder="Компенсация, возврат, нарушение…" required>
@error('reason')<span class="form-error">{{ $message }}</span>@enderror
</label>
<button class="button button--gold button--full" type="submit">Провести операцию</button>
</form>
</article>
<article class="content-card">
<h3>Правила операций</h3>
<p>Количество нельзя сделать отрицательным. Каждая корректировка содержит ID администратора, логин, причину и итоговое значение.</p>
<p>Операции необратимы в журнале: ошибочное начисление исправляется отдельным списанием.</p>
</article>
</div>
</section>
<section class="section">
<div class="section-heading">
<span class="eyebrow">Аудит</span>
<h2>Последние транзакции</h2>
</div>
<form class="search-form" method="GET" action="{{ route('admin.balances.index') }}">
<input class="form-control" type="search" name="search" value="{{ $search }}" placeholder="Логин или ID аккаунта">
<button class="button button--gold" type="submit">Найти</button>
@if ($search !== '')
<a class="button button--ghost" href="{{ route('admin.balances.index') }}">Сбросить</a>
@endif
</form>
<div class="account-list balance-ledger">
@forelse ($transactions as $transaction)
@php($meta = $transaction->metadata ?? [])
<article class="content-card balance-ledger__row">
<div>
<strong>{{ $transaction->username ?: 'Аккаунт удалён' }}</strong>
<span>ID #{{ $transaction->account_id }} · {{ $transaction->created_at->format('d.m.Y H:i:s') }}</span>
</div>
<div>
<strong>{{ match ($transaction->type) {
'deposit' => 'Пополнение',
'spend' => 'Покупка в игре',
'admin_credit' => 'Начисление администратором',
'admin_debit' => 'Списание администратором',
default => $transaction->type,
} }}</strong>
<span>{{ $meta['reason'] ?? $meta['description'] ?? $transaction->reference }}</span>
@if (isset($meta['admin_username']))
<span>Администратор: {{ $meta['admin_username'] }} (#{{ $meta['admin_account_id'] }})</span>
@endif
</div>
<div class="balance-ledger__amount">
<strong class="{{ (float) $transaction->amount >= 0 ? 'balance-operation--positive' : '' }}">
{{ (float) $transaction->amount >= 0 ? '+' : '' }}{{ number_format((float) $transaction->amount, 2, ',', ' ') }}
</strong>
<span>Слёз Элуны: {{ number_format((float) $transaction->balance_after, 2, ',', ' ') }}</span>
</div>
</article>
@empty
<article class="content-card empty-state"><p>Транзакции не найдены.</p></article>
@endforelse
</div>
</section>
@endsection
@@ -1,165 +0,0 @@
@extends('layouts.portal')
@section('portal-content')
<section class="section">
<div class="section-heading">
<span class="eyebrow">Админка</span>
<h2>Новости логин-экрана WoW</h2>
<p>Публикуй raw-текст с WoW-разметкой, а сайт сам отдаст готовый `SERVERALERT`-фид по открытой ссылке для клиента 3.3.5a.</p>
</div>
<div class="portal-grid">
<article class="content-card">
<h3>Публичная ссылка для клиента</h3>
<p>Эту ссылку можно вставить в логин-скрин клиента. Ответ возвращается как plain text и доступен без авторизации.</p>
<div class="action-stack">
<a class="button button--gold" href="{{ $feedUrl }}" target="_blank" rel="noreferrer">Открыть фид</a>
<code class="code-block">{{ $feedUrl }}</code>
</div>
<dl class="stats-list stats-list--compact">
<div>
<dt>Всего записей</dt>
<dd>{{ $newsStats['total'] }}</dd>
</div>
<div>
<dt>Опубликовано</dt>
<dd>{{ $newsStats['published'] }}</dd>
</div>
<div>
<dt>Черновиков</dt>
<dd>{{ $newsStats['draft'] }}</dd>
</div>
</dl>
</article>
<article class="content-card">
<h3>Итоговый ответ сервера</h3>
<p>Это превью полного ответа, который получит клиент по публичному URL.</p>
<pre class="feed-preview">{{ $feedPreview }}</pre>
</article>
</div>
</section>
<section class="section section--split">
<article class="content-card">
<span class="eyebrow">Новая запись</span>
<h2>Добавить новость</h2>
<p>Каждая запись становится отдельным блоком в общем фиде. Префикс `SERVERALERT:` добавляется автоматически.</p>
<form class="stack-form" method="POST" action="{{ route('admin.login-screen-news.store') }}">
@csrf
<label class="form-field">
<span>Название в админке</span>
<input class="form-control @error('title') is-invalid @enderror" type="text" name="title" value="{{ old('title') }}" maxlength="120" required>
@error('title')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="form-field">
<span>Порядок вывода</span>
<input class="form-control @error('sort_order') is-invalid @enderror" type="number" name="sort_order" value="{{ old('sort_order', 0) }}" min="0" max="9999" required>
@error('sort_order')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="checkbox-inline">
<input type="checkbox" name="is_published" value="1" @checked(old('is_published', true))>
<span>Сразу публиковать эту запись</span>
</label>
<label class="form-field">
<span>Текст для клиента</span>
<textarea class="form-control form-control--textarea @error('body') is-invalid @enderror" name="body" required>{{ old('body') }}</textarea>
<small>Поддерживаются WoW-коды цветов `|cFF...|r`, иконки `|T...|t`, переносы строк и обычный текст.</small>
@error('body')<span class="form-error">{{ $message }}</span>@enderror
</label>
<button class="button button--gold" type="submit">Добавить запись</button>
</form>
</article>
<article class="content-card">
<span class="eyebrow">Шаблон</span>
<h2>Пример WoW-блока</h2>
<p>Если нужно, можно вставлять уже готовые строки почти в том же виде, как у тебя в примере с GitHub raw.</p>
<pre class="feed-preview feed-preview--compact">|TInterface\ICONS\achievement_boss_lichking:25:25:0:-5|t |cFF00D1FFWrath of the Lich King|r
|TInterface\ICONS\achievement_zone_icecrown_01:18:18:0:-8|t |cFFA6D8FF- Артас пробуждается как Король-лич|r
|TInterface\ICONS\spell_deathknight_iceboundfortitude:18:18:0:-8|t |cFFA6D8FF- Рыцари смерти вторгаются в Азерот|r</pre>
</article>
</section>
<section class="section">
<div class="section-heading">
<span class="eyebrow">Текущие записи</span>
<h2>Редактирование и публикация</h2>
<p>Опубликованные записи попадают в фид по порядку `sort_order`. Черновики остаются только в админке.</p>
</div>
<div class="account-list">
@forelse ($newsItems as $newsItem)
<article class="content-card news-card">
<div class="account-card__header">
<div>
<h3>{{ $newsItem->title }}</h3>
<p>
Порядок: {{ $newsItem->sort_order }}
@if ($newsItem->updated_by_username)
· Обновил: {{ $newsItem->updated_by_username }}
@endif
@if ($newsItem->updated_at)
· {{ $newsItem->updated_at->format('d.m.Y H:i') }}
@endif
</p>
</div>
<span class="tag">{{ $newsItem->is_published ? 'Опубликовано' : 'Черновик' }}</span>
</div>
<form class="stack-form" method="POST" action="{{ route('admin.login-screen-news.update', $newsItem) }}">
@csrf
@method('PATCH')
<label class="form-field">
<span>Название</span>
<input class="form-control" type="text" name="title" value="{{ $newsItem->title }}" maxlength="120" required>
</label>
<label class="form-field">
<span>Порядок вывода</span>
<input class="form-control" type="number" name="sort_order" value="{{ $newsItem->sort_order }}" min="0" max="9999" required>
</label>
<label class="checkbox-inline">
<input type="checkbox" name="is_published" value="1" @checked($newsItem->is_published)>
<span>Показывать в публичном фиде</span>
</label>
<label class="form-field">
<span>Текст блока</span>
<textarea class="form-control form-control--textarea" name="body" required>{{ $newsItem->body }}</textarea>
</label>
<div class="form-actions">
<button class="button button--gold" type="submit">Сохранить</button>
</div>
</form>
<form method="POST" action="{{ route('admin.login-screen-news.destroy', $newsItem) }}">
@csrf
@method('DELETE')
<button class="button button--ghost" type="submit" onclick="return confirm('Удалить эту запись из фида?')">Удалить запись</button>
</form>
</article>
@empty
<article class="content-card empty-state">
<h3>Записей пока нет</h3>
<p>Добавь первую новость, и публичный фид сразу начнёт отдавать её клиенту.</p>
</article>
@endforelse
</div>
</section>
@endsection
-154
View File
@@ -1,154 +0,0 @@
@extends('layouts.portal')
@section('portal-content')
<section class="section">
<div class="section-heading">
<span class="eyebrow">Админка</span>
<h2>Новости</h2>
<p>Новости отображаются в лаунчере. Картинка загружается в S3-хранилище.</p>
</div>
<div class="portal-grid">
<article class="content-card">
<h3>Статистика</h3>
<dl class="stats-list stats-list--compact">
<div>
<dt>Всего</dt>
<dd>{{ $newsStats['total'] }}</dd>
</div>
<div>
<dt>Опубликовано</dt>
<dd>{{ $newsStats['published'] }}</dd>
</div>
<div>
<dt>Черновиков</dt>
<dd>{{ $newsStats['draft'] }}</dd>
</div>
</dl>
</article>
</div>
</section>
<section class="section">
<article class="content-card">
<span class="eyebrow">Новая запись</span>
<h2>Добавить новость</h2>
<form class="stack-form" method="POST" action="{{ route('admin.news.store') }}" enctype="multipart/form-data">
@csrf
<label class="form-field">
<span>Заголовок</span>
<input class="form-control @error('title') is-invalid @enderror" type="text" name="title" value="{{ old('title') }}" maxlength="120" required>
@error('title')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="form-field">
<span>Порядок вывода</span>
<input class="form-control @error('sort_order') is-invalid @enderror" type="number" name="sort_order" value="{{ old('sort_order', 0) }}" min="0" max="9999" required>
@error('sort_order')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="checkbox-inline">
<input type="checkbox" name="is_published" value="1" @checked(old('is_published', true))>
<span>Сразу опубликовать</span>
</label>
<label class="form-field">
<span>Текст новости</span>
<textarea class="form-control form-control--textarea @error('body') is-invalid @enderror" name="body" required>{{ old('body') }}</textarea>
@error('body')<span class="form-error">{{ $message }}</span>@enderror
</label>
<label class="form-field">
<span>Картинка</span>
<input class="form-control @error('image') is-invalid @enderror" type="file" name="image" accept="image/*">
<small>Максимум 2 МБ. Необязательно.</small>
@error('image')<span class="form-error">{{ $message }}</span>@enderror
</label>
<button class="button button--gold" type="submit">Добавить новость</button>
</form>
</article>
</section>
<section class="section">
<div class="section-heading">
<span class="eyebrow">Текущие записи</span>
<h2>Редактирование</h2>
</div>
<div class="account-list">
@forelse ($newsItems as $newsItem)
<article class="content-card news-card">
<div class="account-card__header">
<div>
<h3>{{ $newsItem->title }}</h3>
<p>
Порядок: {{ $newsItem->sort_order }}
· {{ $newsItem->created_by_username ?? '—' }}
@if ($newsItem->updated_at)
· {{ $newsItem->updated_at->format('d.m.Y H:i') }}
@endif
</p>
</div>
<span class="tag">{{ $newsItem->is_published ? 'Опубликовано' : 'Черновик' }}</span>
</div>
@if ($newsItem->image_url)
<img src="{{ $newsItem->image_url }}" alt="{{ $newsItem->title }}" style="max-width: 320px; border-radius: 6px; margin-bottom: 1rem;">
@endif
<form class="stack-form" method="POST" action="{{ route('admin.news.update', $newsItem) }}" enctype="multipart/form-data">
@csrf
@method('PATCH')
<label class="form-field">
<span>Заголовок</span>
<input class="form-control" type="text" name="title" value="{{ $newsItem->title }}" maxlength="120" required>
</label>
<label class="form-field">
<span>Порядок вывода</span>
<input class="form-control" type="number" name="sort_order" value="{{ $newsItem->sort_order }}" min="0" max="9999" required>
</label>
<label class="checkbox-inline">
<input type="checkbox" name="is_published" value="1" @checked($newsItem->is_published)>
<span>Опубликовано</span>
</label>
<label class="form-field">
<span>Текст новости</span>
<textarea class="form-control form-control--textarea" name="body" required>{{ $newsItem->body }}</textarea>
</label>
<label class="form-field">
<span>Заменить картинку</span>
<input class="form-control" type="file" name="image" accept="image/*">
<small>Оставь пустым, чтобы сохранить текущую.</small>
</label>
<div class="form-actions">
<button class="button button--gold" type="submit">Сохранить</button>
</div>
</form>
<form method="POST" action="{{ route('admin.news.destroy', $newsItem) }}">
@csrf
@method('DELETE')
<button class="button button--ghost" type="submit" onclick="return confirm('Удалить эту новость?')">Удалить</button>
</form>
</article>
@empty
<article class="content-card empty-state">
<h3>Новостей пока нет</h3>
<p>Добавь первую новость — она сразу появится в лаунчере.</p>
</article>
@endforelse
</div>
</section>
@endsection
@@ -1,76 +0,0 @@
@extends('layouts.portal')
@section('portal-content')
<section class="section">
<div class="section-heading">
<span class="eyebrow">Игровой сервер</span>
<h2>Магазин</h2>
<p>Изменения записываются прямо в базу <code>store</code>. После сохранения перезагрузи данные магазина командой игрового модуля или перезапусти worldserver.</p>
</div>
<div class="portal-grid">
<article class="content-card">
<h3>Новая категория</h3>
<form class="stack-form" method="POST" action="{{ route('admin.shop.categories.store') }}">
@csrf
@include('admin.shop.partials.category-fields', ['category' => null])
<button class="button button--gold" type="submit">Создать категорию</button>
</form>
</article>
<article class="content-card">
<h3>Новый товар или услуга</h3>
@if ($categories->isEmpty())
<p>Сначала создай категорию.</p>
@else
<form class="stack-form" method="POST" action="{{ route('admin.shop.products.store') }}">
@csrf
@include('admin.shop.partials.product-fields', ['product' => null])
<button class="button button--gold" type="submit">Создать позицию</button>
</form>
@endif
</article>
</div>
</section>
<section class="section">
<div class="section-heading"><span class="eyebrow">Навигация магазина</span><h2>Категории</h2></div>
<div class="account-list">
@forelse ($categories as $category)
<article class="content-card">
<div class="account-card__header"><h3>#{{ $category->id }} · {{ $category->name }}</h3><span class="tag">{{ $category->products_count }} позиций</span></div>
<form class="stack-form" method="POST" action="{{ route('admin.shop.categories.update', $category) }}">
@csrf @method('PATCH')
@include('admin.shop.partials.category-fields', ['category' => $category])
<button class="button button--gold" type="submit">Сохранить</button>
</form>
<form method="POST" action="{{ route('admin.shop.categories.destroy', $category) }}">@csrf @method('DELETE')<button class="button button--ghost" onclick="return confirm('Удалить категорию и её связи с товарами?')">Удалить</button></form>
</article>
@empty
<article class="content-card empty-state"><p>Категорий пока нет.</p></article>
@endforelse
</div>
</section>
<section class="section">
<div class="section-heading"><span class="eyebrow">Контент магазина</span><h2>Товары и услуги</h2></div>
<div class="account-list">
@forelse ($products as $product)
<article class="content-card news-card">
<div class="account-card__header">
<div><h3>#{{ $product->id }} · {!! nl2br(e($product->name)) !!}</h3><p>{{ $product->categories->pluck('name')->join(', ') ?: 'Без категории' }} · тип {{ $product->type }}</p></div>
<span class="tag">{{ $product->price }} Слёз Элуны</span>
</div>
<form class="stack-form" method="POST" action="{{ route('admin.shop.products.update', $product) }}">
@csrf @method('PATCH')
@include('admin.shop.partials.product-fields', ['product' => $product])
<button class="button button--gold" type="submit">Сохранить</button>
</form>
<form method="POST" action="{{ route('admin.shop.products.destroy', $product) }}">@csrf @method('DELETE')<button class="button button--ghost" onclick="return confirm('Удалить позицию магазина?')">Удалить</button></form>
</article>
@empty
<article class="content-card empty-state"><p>Позиций пока нет.</p></article>
@endforelse
</div>
</section>
@endsection
@@ -1,6 +0,0 @@
@php($category = $category ?? null)
<label class="form-field"><span>Название</span><input class="form-control" name="name" value="{{ $category?->name }}" maxlength="120" required></label>
<label class="form-field"><span>Иконка клиента</span><input class="form-control" name="icon" value="{{ $category?->icon }}" placeholder="inv_helmet_96" maxlength="255"></label>
<label class="form-field"><span>Требуемый ранг</span><input class="form-control" type="number" name="requiredRank" value="{{ $category?->requiredRank ?? 0 }}" min="0" max="255" required></label>
<label class="form-field"><span>Флаги</span><input class="form-control" type="number" name="flags" value="{{ $category?->flags ?? 0 }}" min="0" required></label>
<label class="checkbox-inline"><input type="checkbox" name="enabled" value="1" @checked($category?->enabled ?? true)><span>Включена</span></label>
@@ -1,28 +0,0 @@
@php($product = $product ?? null)
@php($selectedCategories = $product?->categories?->pluck('id')->all() ?? [])
<label class="form-field"><span>Категории</span><select class="form-control" name="category_ids[]" multiple size="{{ min(max($categories->count(), 2), 8) }}" required>@foreach ($categories as $category)<option value="{{ $category->id }}" @selected(in_array($category->id, $selectedCategories, true))>{{ $category->name }}</option>@endforeach</select><small>Можно выбрать несколько с Ctrl/Cmd.</small></label>
<label class="form-field"><span>Тип</span><select class="form-control" name="type" required>@foreach ([1 => 'Предмет', 3 => 'Маунт', 4 => 'Питомец', 5 => 'Бафф', 7 => 'Услуга персонажа', 8 => 'Буст', 9 => 'Титул'] as $type => $label)<option value="{{ $type }}" @selected(($product?->type ?? 1) === $type)>{{ $label }} ({{ $type }})</option>@endforeach</select></label>
<label class="form-field"><span>Название</span><textarea class="form-control" name="name" required>{{ $product?->name }}</textarea></label>
<label class="form-field"><span>Название подсказки</span><input class="form-control" name="tooltipName" value="{{ $product?->tooltipName }}"></label>
<label class="form-field"><span>Тип подсказки</span><input class="form-control" name="tooltipType" value="{{ $product?->tooltipType }}" placeholder="item или spell"></label>
<label class="form-field"><span>Текст подсказки</span><textarea class="form-control form-control--textarea" name="tooltipText">{{ $product?->tooltipText }}</textarea></label>
<label class="form-field"><span>Иконка клиента</span><input class="form-control" name="icon" value="{{ $product?->icon }}" placeholder="inv_sword_22"></label>
<div class="form-row">
<label class="form-field"><span>Цена, Слёзы Элуны</span><input class="form-control" type="number" name="price" value="{{ $product?->price ?? 1 }}" min="1" required></label>
<label class="form-field"><span>Скидка, %</span><input class="form-control" type="number" name="discountAmount" value="{{ $product?->discountAmount ?? 0 }}" min="0" max="100"></label>
</div>
<div class="form-row">
<label class="form-field"><span>Hyperlink ID</span><input class="form-control" type="number" name="hyperlinkId" value="{{ $product?->hyperlinkId ?? 0 }}" min="0"></label>
<label class="form-field"><span>Creature Entry</span><input class="form-control" type="number" name="creatureEntry" value="{{ $product?->creatureEntry ?? 0 }}" min="0"></label>
<label class="form-field"><span>Флаги</span><input class="form-control" type="number" name="flags" value="{{ $product?->flags ?? 0 }}" min="0"></label>
</div>
<div class="shop-reward-grid">
@foreach (range(1, 8) as $slot)
<label class="form-field"><span>Награда {{ $slot }}</span><input class="form-control" type="number" name="reward_{{ $slot }}" value="{{ $product?->{'reward_'.$slot} ?? 0 }}" min="0"></label>
<label class="form-field"><span>Количество {{ $slot }}</span><input class="form-control" type="number" name="rewardcount_{{ $slot }}" value="{{ $product?->{'rewardcount_'.$slot} ?? 0 }}" min="0"></label>
@endforeach
</div>
<div class="form-actions">
<label class="checkbox-inline"><input type="checkbox" name="new" value="1" @checked($product?->new ?? false)><span>Пометка «Новинка»</span></label>
<label class="checkbox-inline"><input type="checkbox" name="enabled" value="1" @checked($product?->enabled ?? true)><span>Включён</span></label>
</div>
-61
View File
@@ -1,61 +0,0 @@
@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
-190
View File
@@ -1,190 +0,0 @@
@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>{{ number_format((float) $balance, 2, ',', ' ') }}</h2>
<p>Пополняй счёт и используй Слёзы Элуны для покупок в игре.</p>
<form class="register-form" method="POST" action="{{ route('cabinet.balance.deposit') }}">
@csrf
<label class="form-field">
<span>Сумма пополнения, ₽</span>
<input class="form-control @error('amount') is-invalid @enderror" type="number" name="amount" min="{{ $minimumDeposit }}" max="{{ $maximumDeposit }}" step="0.01" value="{{ old('amount', 500) }}" required>
@error('amount')<span class="form-error">{{ $message }}</span>@enderror
</label>
<button class="button button--gold button--full" type="submit">Пополнить</button>
<span class="muted-note">1 ₽ = {{ $tearsPerRuble }} Слёз Элуны</span>
<span class="muted-note">От {{ $minimumDeposit }} до {{ number_format((float) $maximumDeposit, 0, ',', ' ') }} ₽</span>
</form>
</article>
<article class="content-card">
<span class="eyebrow">История</span>
<h2>Последние операции</h2>
@forelse ($balanceTransactions as $transaction)
<div class="balance-operation">
<div>
<strong>{{ $transaction->type === 'deposit' ? 'Пополнение' : 'Покупка в игре' }}</strong>
<span>{{ $transaction->created_at->format('d.m.Y H:i') }}</span>
</div>
<strong class="{{ (float) $transaction->amount >= 0 ? 'balance-operation--positive' : '' }}">
{{ (float) $transaction->amount >= 0 ? '+' : '' }}{{ number_format((float) $transaction->amount, 2, ',', ' ') }}
</strong>
</div>
@empty
<div class="empty-inline">Операций пока нет.</div>
@endforelse
</article>
</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
View File
@@ -1,17 +0,0 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('moonwell.realm.server_name') }} — регистрация WoW аккаунта</title>
<meta name="description" content="Лендинг и форма регистрации игрового аккаунта для сервера Moonwell.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{{ asset('site.css') }}">
</head>
<body>
@yield('content')
@include('partials.yandex-metrika')
</body>
</html>
-55
View File
@@ -1,55 +0,0 @@
@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>
<a href="{{ route('admin.balances.index') }}">Слёзы Элуны</a>
<a href="{{ route('admin.shop.index') }}">Магазин</a>
<a href="{{ route('admin.login-screen-news.index') }}">Новости WoW</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