diff --git a/.env.example b/.env.example index b41ad05..635ea3a 100644 --- a/.env.example +++ b/.env.example @@ -59,6 +59,11 @@ MAIL_PASSWORD=null MAIL_FROM_ADDRESS="hello@example.com" MAIL_FROM_NAME="${APP_NAME}" +MOONWELL_LEGAL_PUBLISHED=false +MOONWELL_LEGAL_OPERATOR="Администрация проекта MoonWell" +MOONWELL_LEGAL_EMAIL="support@moon-well.online" +MOONWELL_LEGAL_EFFECTIVE_DATE="25 июля 2026 года" + AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=ru-central1 diff --git a/app/Http/Controllers/Admin/AccountController.php b/app/Http/Controllers/Admin/AccountController.php index aa3f46e..501def1 100644 --- a/app/Http/Controllers/Admin/AccountController.php +++ b/app/Http/Controllers/Admin/AccountController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin; +use App\Http\Controllers\Concerns\BuildsPortalProps; use App\Http\Controllers\Controller; use App\Http\Requests\UpdateAccountAccessRequest; use App\Services\AzerothCoreAccountService; @@ -9,16 +10,18 @@ use App\Services\InviteCodeService; use App\Support\WowData; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\View\View; +use Inertia\Inertia; +use Inertia\Response; class AccountController extends Controller { + use BuildsPortalProps; + public function index( Request $request, AzerothCoreAccountService $accounts, InviteCodeService $inviteCodes, - ): View - { + ): Response { $search = trim((string) $request->query('search', '')); $searchTerm = $search !== '' ? $search : null; $activeTab = (string) $request->query('tab', AzerothCoreAccountService::ACCOUNT_TAB_PLAYERS); @@ -29,8 +32,32 @@ class AccountController extends Controller $accountStats = $accounts->accountTypeStats($searchTerm); - return view('admin.accounts.index', [ - 'accounts' => $accounts->listAccounts($searchTerm, $activeTab), + $accountItems = $accounts->listAccounts($searchTerm, $activeTab) + ->map(fn (array $account): array => [ + ...collect($account)->except('characters')->all(), + 'email' => $account['email'] !== '' ? $account['email'] : 'email не указан', + 'status_label' => $account['locked'] ? 'Заблокирован' : 'Активен', + 'characters' => $account['characters']->values()->all(), + 'update_access_url' => route('admin.accounts.access.update', $account['id']), + ]) + ->values() + ->all(); + + $invites = $inviteCodes->latest() + ->map(fn ($invite): array => [ + 'id' => $invite->id, + 'code' => $invite->code, + 'created_by' => $invite->created_by_username ?: '—', + 'created_at' => $invite->created_at?->format('d.m.Y H:i'), + 'redeemed' => $invite->redeemed_at !== null, + 'redeemed_by' => $invite->redeemed_by_username ?: 'ещё не использован', + ]) + ->values() + ->all(); + + return Inertia::render('Admin/Accounts', [ + ...$this->portalProps($request), + 'accounts' => $accountItems, 'accountStats' => $accountStats, 'accountTabs' => [ AzerothCoreAccountService::ACCOUNT_TAB_PLAYERS => [ @@ -43,10 +70,18 @@ class AccountController extends Controller ], ], 'activeTab' => $activeTab, - 'inviteCodes' => $inviteCodes->latest(), + 'inviteCodes' => $invites, 'inviteStats' => $inviteCodes->stats(), + 'inviteStoreUrl' => route('admin.invite-codes.store'), + 'registrationRequiresInvite' => (bool) config('moonwell.registration.require_invite_code'), 'search' => $search, - 'accessLevels' => WowData::securityLevels(), + 'accessLevels' => collect(WowData::securityLevels()) + ->map(fn (string $label, int $value): array => compact('value', 'label')) + ->values() + ->all(), + 'old' => [ + 'quantity' => $request->old('quantity', 1), + ], ]); } diff --git a/app/Http/Controllers/Admin/BalanceController.php b/app/Http/Controllers/Admin/BalanceController.php index aef8e6d..d2da9ac 100644 --- a/app/Http/Controllers/Admin/BalanceController.php +++ b/app/Http/Controllers/Admin/BalanceController.php @@ -2,24 +2,66 @@ namespace App\Http\Controllers\Admin; +use App\Http\Controllers\Concerns\BuildsPortalProps; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\AdjustBalanceRequest; use App\Services\AzerothCoreAccountService; use App\Services\BalanceService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\View\View; +use Inertia\Inertia; +use Inertia\Response; use InvalidArgumentException; class BalanceController extends Controller { - public function index(Request $request, BalanceService $balances): View + use BuildsPortalProps; + + public function index(Request $request, BalanceService $balances): Response { $search = trim((string) $request->query('search', '')); - return view('admin.balances.index', [ - 'transactions' => $balances->adminTransactions($search !== '' ? $search : null), + $transactions = $balances->adminTransactions($search !== '' ? $search : null) + ->map(function ($transaction): array { + $metadata = $transaction->metadata ?? []; + + return [ + 'id' => $transaction->id, + 'username' => $transaction->username ?: 'Аккаунт удалён', + 'account_id' => $transaction->account_id, + 'created_at' => $transaction->created_at?->format('d.m.Y H:i:s'), + 'type_label' => match ($transaction->type) { + 'deposit' => 'Пополнение', + 'spend' => 'Покупка в игре', + 'admin_credit' => 'Начисление администратором', + 'admin_debit' => 'Списание администратором', + default => $transaction->type, + }, + 'description' => $metadata['reason'] ?? $metadata['description'] ?? $transaction->reference, + 'admin' => isset($metadata['admin_username']) ? [ + 'username' => $metadata['admin_username'], + 'account_id' => $metadata['admin_account_id'], + ] : null, + 'amount' => (string) $transaction->amount, + 'formatted_amount' => number_format((float) $transaction->amount, 2, ',', ' '), + 'is_positive' => (float) $transaction->amount >= 0, + 'formatted_balance_after' => number_format((float) $transaction->balance_after, 2, ',', ' '), + ]; + }) + ->values() + ->all(); + + return Inertia::render('Admin/Balances', [ + ...$this->portalProps($request), + 'transactions' => $transactions, 'search' => $search, + 'adjustUrl' => route('admin.balances.adjust'), + 'old' => [ + 'account' => $request->old('account', ''), + 'direction' => $request->old('direction', 'credit'), + 'amount' => $request->old('amount', ''), + 'reason' => $request->old('reason', ''), + ], ]); } diff --git a/app/Http/Controllers/Admin/LoginScreenNewsController.php b/app/Http/Controllers/Admin/LoginScreenNewsController.php index fdf7aa6..8e24b2a 100644 --- a/app/Http/Controllers/Admin/LoginScreenNewsController.php +++ b/app/Http/Controllers/Admin/LoginScreenNewsController.php @@ -2,23 +2,49 @@ namespace App\Http\Controllers\Admin; +use App\Http\Controllers\Concerns\BuildsPortalProps; use App\Http\Controllers\Controller; use App\Http\Requests\SaveLoginScreenNewsRequest; use App\Models\LoginScreenNews; use App\Services\LoginScreenNewsService; use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Illuminate\Support\Str; -use Illuminate\View\View; +use Inertia\Inertia; +use Inertia\Response; class LoginScreenNewsController extends Controller { - public function index(LoginScreenNewsService $news): View + use BuildsPortalProps; + + public function index(Request $request, LoginScreenNewsService $news): Response { - return view('admin.login-screen-news.index', [ - 'newsItems' => $news->listForAdmin(), + return Inertia::render('Admin/LoginScreenNews', [ + ...$this->portalProps($request), + 'newsItems' => $news->listForAdmin() + ->map(fn (LoginScreenNews $item): array => [ + 'id' => $item->id, + 'title' => $item->title, + 'body' => $item->body, + 'sort_order' => $item->sort_order, + 'is_published' => $item->is_published, + 'updated_by' => $item->updated_by_username, + 'updated_at' => $item->updated_at?->format('d.m.Y H:i'), + 'update_url' => route('admin.login-screen-news.update', $item), + 'destroy_url' => route('admin.login-screen-news.destroy', $item), + ]) + ->values() + ->all(), 'newsStats' => $news->stats(), 'feedPreview' => $news->renderFeed(), 'feedUrl' => Str::replaceFirst('https://', 'http://', route('launcher.login-news.show')), + 'storeUrl' => route('admin.login-screen-news.store'), + 'old' => [ + 'title' => $request->old('title', ''), + 'body' => $request->old('body', ''), + 'sort_order' => $request->old('sort_order', 0), + 'is_published' => (bool) $request->old('is_published', true), + ], ]); } diff --git a/app/Http/Controllers/Admin/NewsController.php b/app/Http/Controllers/Admin/NewsController.php index 116c5f9..95ecc0b 100644 --- a/app/Http/Controllers/Admin/NewsController.php +++ b/app/Http/Controllers/Admin/NewsController.php @@ -2,20 +2,47 @@ namespace App\Http\Controllers\Admin; +use App\Http\Controllers\Concerns\BuildsPortalProps; use App\Http\Controllers\Controller; use App\Http\Requests\SaveNewsRequest; use App\Models\News; use App\Services\NewsService; use Illuminate\Http\RedirectResponse; -use Illuminate\View\View; +use Illuminate\Http\Request; +use Inertia\Inertia; +use Inertia\Response; class NewsController extends Controller { - public function index(NewsService $news): View + use BuildsPortalProps; + + public function index(Request $request, NewsService $news): Response { - return view('admin.news.index', [ - 'newsItems' => $news->listForAdmin(), + return Inertia::render('Admin/News', [ + ...$this->portalProps($request), + 'newsItems' => $news->listForAdmin() + ->map(fn (News $item): array => [ + 'id' => $item->id, + 'title' => $item->title, + 'body' => $item->body, + 'sort_order' => $item->sort_order, + 'is_published' => $item->is_published, + 'image_url' => $item->image_url, + 'created_by' => $item->created_by_username ?? '—', + 'updated_at' => $item->updated_at?->format('d.m.Y H:i'), + 'update_url' => route('admin.news.update', $item), + 'destroy_url' => route('admin.news.destroy', $item), + ]) + ->values() + ->all(), 'newsStats' => $news->stats(), + 'storeUrl' => route('admin.news.store'), + 'old' => [ + 'title' => $request->old('title', ''), + 'body' => $request->old('body', ''), + 'sort_order' => $request->old('sort_order', 0), + 'is_published' => (bool) $request->old('is_published', true), + ], ]); } diff --git a/app/Http/Controllers/Admin/ShopController.php b/app/Http/Controllers/Admin/ShopController.php index 6d01db7..508e97b 100644 --- a/app/Http/Controllers/Admin/ShopController.php +++ b/app/Http/Controllers/Admin/ShopController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Admin; +use App\Http\Controllers\Concerns\BuildsPortalProps; use App\Http\Controllers\Controller; use App\Http\Requests\Admin\SaveShopCategoryRequest; use App\Http\Requests\Admin\SaveShopProductRequest; @@ -9,15 +10,73 @@ use App\Models\ShopCategory; use App\Models\ShopProduct; use App\Services\ShopCatalogService; use Illuminate\Http\RedirectResponse; -use Illuminate\View\View; +use Illuminate\Http\Request; +use Inertia\Inertia; +use Inertia\Response; class ShopController extends Controller { - public function index(ShopCatalogService $catalog): View + use BuildsPortalProps; + + public function index(Request $request, ShopCatalogService $catalog): Response { - return view('admin.shop.index', [ - 'categories' => $catalog->categories(), - 'products' => $catalog->products(), + $categories = $catalog->categories(); + + return Inertia::render('Admin/Shop', [ + ...$this->portalProps($request), + 'categories' => $categories + ->map(fn (ShopCategory $category): array => [ + 'id' => $category->id, + 'name' => $category->name, + 'icon' => $category->icon, + 'required_rank' => $category->requiredRank, + 'flags' => $category->flags, + 'enabled' => $category->enabled, + 'products_count' => $category->products_count, + 'update_url' => route('admin.shop.categories.update', $category), + 'destroy_url' => route('admin.shop.categories.destroy', $category), + ]) + ->values() + ->all(), + 'products' => $catalog->products() + ->map(fn (ShopProduct $product): array => [ + 'id' => $product->id, + 'category_ids' => $product->categories->pluck('id')->values()->all(), + 'category_names' => $product->categories->pluck('name')->values()->all(), + 'type' => (int) $product->type, + 'name' => $product->name, + 'tooltip_name' => $product->tooltipName, + 'tooltip_type' => $product->tooltipType, + 'tooltip_text' => $product->tooltipText, + 'icon' => $product->icon, + 'price' => (int) $product->price, + 'hyperlink_id' => (int) $product->hyperlinkId, + 'creature_entry' => (int) $product->creatureEntry, + 'discount_amount' => (int) $product->discountAmount, + 'flags' => (int) $product->flags, + 'rewards' => collect(range(1, 8))->map(fn (int $slot): array => [ + 'slot' => $slot, + 'id' => (int) $product->{'reward_'.$slot}, + 'count' => (int) $product->{'rewardcount_'.$slot}, + ])->all(), + 'is_new' => (bool) $product->new, + 'enabled' => (bool) $product->enabled, + 'update_url' => route('admin.shop.products.update', $product), + 'destroy_url' => route('admin.shop.products.destroy', $product), + ]) + ->values() + ->all(), + 'categoryStoreUrl' => route('admin.shop.categories.store'), + 'productStoreUrl' => route('admin.shop.products.store'), + 'productTypes' => collect([ + 1 => 'Предмет', + 3 => 'Маунт', + 4 => 'Питомец', + 5 => 'Бафф', + 7 => 'Услуга персонажа', + 8 => 'Буст', + 9 => 'Титул', + ])->map(fn (string $label, int $value): array => compact('value', 'label'))->values()->all(), ]); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 9867f70..679336f 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -7,13 +7,39 @@ use App\Http\Requests\LoginRequest; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; -use Illuminate\View\View; +use Inertia\Inertia; +use Inertia\Response; class LoginController extends Controller { - public function create(): View + public function create(Request $request): Response { - return view('auth.login'); + return Inertia::render('Auth/Login', [ + 'site' => [ + 'name' => config('moonwell.realm.server_name'), + 'tagline' => config('moonwell.realm.tagline'), + ], + 'auth' => [ + 'authenticated' => false, + 'username' => null, + 'access_label' => null, + 'is_admin' => false, + ], + 'urls' => [ + 'home' => route('landing.index'), + 'login' => route('login'), + 'login_store' => route('login.store'), + 'offer' => config('moonwell.legal.published') ? route('legal.offer') : null, + 'privacy' => config('moonwell.legal.published') ? route('legal.privacy') : null, + ], + 'flash' => [ + 'status' => $request->session()->get('status'), + 'error' => $request->session()->get('error'), + ], + 'old' => [ + 'username' => $request->old('username', ''), + ], + ]); } public function store(LoginRequest $request): RedirectResponse diff --git a/app/Http/Controllers/CabinetController.php b/app/Http/Controllers/CabinetController.php index b96dd5a..805061b 100644 --- a/app/Http/Controllers/CabinetController.php +++ b/app/Http/Controllers/CabinetController.php @@ -8,24 +8,71 @@ use App\Services\BalanceService; use App\Services\GameClientDownloadService; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; -use Illuminate\View\View; +use Inertia\Inertia; +use Inertia\Response; use Throwable; class CabinetController extends Controller { - public function index(Request $request, AzerothCoreAccountService $accounts, BalanceService $balances): View + public function index(Request $request, AzerothCoreAccountService $accounts, BalanceService $balances): Response { $user = $request->user(); + $balance = $balances->balance($user->id); + $characters = $accounts->charactersForAccount($user->id)->values(); - return view('cabinet.index', [ - 'user' => $user, - 'characters' => $accounts->charactersForAccount($user->id), - 'clientFilename' => config('moonwell.client.object_key'), - 'balance' => $balances->balance($user->id), - 'balanceTransactions' => $balances->recentTransactions($user->id), - 'minimumDeposit' => config('services.robokassa.minimum_amount'), - 'maximumDeposit' => config('services.robokassa.maximum_amount'), - 'tearsPerRuble' => config('services.robokassa.tears_per_ruble'), + return Inertia::render('Cabinet/Index', [ + 'site' => $this->siteData(), + 'auth' => [ + 'authenticated' => true, + 'username' => $user->username, + 'access_label' => $user->accessLabel(), + 'is_admin' => $user->canAccessAdminPanel(), + ], + 'urls' => $this->urls(), + 'csrfToken' => csrf_token(), + 'flash' => [ + 'status' => $request->session()->get('status'), + 'error' => $request->session()->get('error'), + ], + 'account' => [ + 'username' => $user->username, + 'email' => $user->email !== '' ? $user->email : '—', + 'access_label' => $user->accessLabel(), + 'joined_at' => $user->joinedAtLabel(), + 'last_login_at' => $user->lastLoginLabel(), + 'locked' => $user->locked, + 'status_label' => $user->locked ? 'Заблокирован' : 'Активен', + ], + 'characters' => $characters->all(), + 'client' => [ + 'filename' => config('moonwell.client.object_key'), + ], + 'wallet' => [ + 'amount' => $balance, + 'formatted_amount' => number_format((float) $balance, 2, ',', ' '), + 'minimum_deposit' => (float) config('services.robokassa.minimum_amount'), + 'maximum_deposit' => (float) config('services.robokassa.maximum_amount'), + 'formatted_maximum_deposit' => number_format((float) config('services.robokassa.maximum_amount'), 0, ',', ' '), + 'tears_per_ruble' => (int) config('services.robokassa.tears_per_ruble'), + 'deposit_amount' => $request->old('amount', 500), + 'transactions' => $balances->recentTransactions($user->id) + ->map(fn ($transaction): array => [ + 'id' => $transaction->id, + 'type' => $transaction->type, + 'label' => match ($transaction->type) { + 'deposit' => 'Пополнение', + 'admin_credit' => 'Начисление', + 'admin_debit' => 'Списание', + default => 'Покупка в игре', + }, + 'amount' => (string) $transaction->amount, + 'formatted_amount' => number_format((float) $transaction->amount, 2, ',', ' '), + 'is_positive' => (float) $transaction->amount >= 0, + 'created_at' => $transaction->created_at?->format('d.m.Y H:i'), + ]) + ->values() + ->all(), + ], ]); } @@ -52,4 +99,33 @@ class CabinetController extends Controller return back()->with('error', 'Не удалось получить ссылку на клиент. Попробуй ещё раз позже.'); } } + + /** + * @return array + */ + private function siteData(): array + { + return [ + 'name' => config('moonwell.realm.server_name'), + 'tagline' => config('moonwell.realm.tagline'), + ]; + } + + /** + * @return array + */ + private function urls(): array + { + return [ + 'home' => route('landing.index'), + 'cabinet' => route('cabinet.index'), + 'admin' => route('admin.accounts.index'), + 'logout' => route('logout'), + 'client' => route('cabinet.client'), + 'password' => route('cabinet.password.update'), + 'deposit' => route('cabinet.balance.deposit'), + 'offer' => config('moonwell.legal.published') ? route('legal.offer') : null, + 'privacy' => config('moonwell.legal.published') ? route('legal.privacy') : null, + ]; + } } diff --git a/app/Http/Controllers/Concerns/BuildsPortalProps.php b/app/Http/Controllers/Concerns/BuildsPortalProps.php new file mode 100644 index 0000000..43130a6 --- /dev/null +++ b/app/Http/Controllers/Concerns/BuildsPortalProps.php @@ -0,0 +1,47 @@ + + */ + protected function portalProps(Request $request): array + { + $user = $request->user(); + + return [ + 'site' => [ + 'name' => config('moonwell.realm.server_name'), + 'tagline' => config('moonwell.realm.tagline'), + ], + 'auth' => [ + 'authenticated' => $user !== null, + 'username' => $user?->username, + 'access_label' => $user?->accessLabel(), + 'is_admin' => $user?->canAccessAdminPanel() ?? false, + ], + 'urls' => [ + 'home' => route('landing.index'), + 'login' => route('login'), + 'cabinet' => route('cabinet.index'), + 'admin' => route('admin.accounts.index'), + 'logout' => route('logout'), + 'offer' => config('moonwell.legal.published') ? route('legal.offer') : null, + 'privacy' => config('moonwell.legal.published') ? route('legal.privacy') : null, + 'admin_accounts' => route('admin.accounts.index'), + 'admin_balances' => route('admin.balances.index'), + 'admin_shop' => route('admin.shop.index'), + 'admin_news' => route('admin.news.index'), + 'admin_login_news' => route('admin.login-screen-news.index'), + ], + 'flash' => [ + 'status' => $request->session()->get('status'), + 'error' => $request->session()->get('error'), + ], + ]; + } +} diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php index eb741f4..88331fa 100644 --- a/app/Http/Controllers/LandingController.php +++ b/app/Http/Controllers/LandingController.php @@ -24,8 +24,7 @@ class LandingController extends Controller Request $request, AzerothCoreAccountService $accounts, NewsService $news, - ): Response - { + ): Response { $realmConfig = config('moonwell.realm'); $rates = $realmConfig['rates'] ?? []; $user = Auth::user(); @@ -78,6 +77,11 @@ class LandingController extends Controller 'moon' => true, ], 'registerEndpoint' => route('game-account.store'), + 'legalUrls' => [ + 'published' => (bool) config('moonwell.legal.published'), + 'offer' => config('moonwell.legal.published') ? route('legal.offer') : null, + 'privacy' => config('moonwell.legal.published') ? route('legal.privacy') : null, + ], 'inviteRequired' => (bool) config('moonwell.registration.require_invite_code'), 'csrfToken' => csrf_token(), 'auth' => [ @@ -108,14 +112,13 @@ class LandingController extends Controller RegisterGameAccountRequest $request, AzerothCoreAccountRegistrar $registrar, InviteCodeService $inviteCodes, - ): RedirectResponse - { + ): RedirectResponse { try { $register = fn (): array => $registrar->register( - $request->string('username')->toString(), - $request->string('email')->toString(), - $request->string('password')->toString(), - ); + $request->string('username')->toString(), + $request->string('email')->toString(), + $request->string('password')->toString(), + ); $result = config('moonwell.registration.require_invite_code') ? $inviteCodes->redeemForRegistration( @@ -152,7 +155,7 @@ class LandingController extends Controller private function errorBag(): array { $errors = session()->get('errors'); - if (!$errors) { + if (! $errors) { return []; } diff --git a/app/Http/Controllers/LegalController.php b/app/Http/Controllers/LegalController.php new file mode 100644 index 0000000..56d3a21 --- /dev/null +++ b/app/Http/Controllers/LegalController.php @@ -0,0 +1,59 @@ +pageData($request)); + } + + public function privacy(Request $request): Response + { + abort_unless(config('moonwell.legal.published'), 404); + + return Inertia::render('Legal/Privacy', $this->pageData($request)); + } + + /** + * @return array + */ + private function pageData(Request $request): array + { + $user = $request->user(); + + return [ + 'site' => [ + 'name' => config('moonwell.realm.server_name'), + 'tagline' => config('moonwell.realm.tagline'), + ], + 'auth' => [ + 'authenticated' => $user !== null, + 'username' => $user?->username, + 'access_label' => $user?->accessLabel(), + 'is_admin' => $user?->canAccessAdminPanel() ?? false, + ], + 'urls' => [ + 'home' => route('landing.index'), + 'login' => route('login'), + 'cabinet' => route('cabinet.index'), + 'admin' => route('admin.accounts.index'), + 'logout' => route('logout'), + 'offer' => config('moonwell.legal.published') ? route('legal.offer') : null, + 'privacy' => config('moonwell.legal.published') ? route('legal.privacy') : null, + ], + 'legal' => [ + 'operator_name' => config('moonwell.legal.operator_name'), + 'contact_email' => config('moonwell.legal.contact_email'), + 'effective_date' => config('moonwell.legal.effective_date'), + ], + ]; + } +} diff --git a/config/moonwell.php b/config/moonwell.php index 5d23330..7fc079c 100644 --- a/config/moonwell.php +++ b/config/moonwell.php @@ -19,6 +19,13 @@ return [ 'bot_account_prefix' => env('MOONWELL_BOT_ACCOUNT_PREFIX', 'RNDBOT'), ], + 'legal' => [ + 'published' => (bool) env('MOONWELL_LEGAL_PUBLISHED', false), + 'operator_name' => env('MOONWELL_LEGAL_OPERATOR', 'Администрация проекта MoonWell'), + 'contact_email' => env('MOONWELL_LEGAL_EMAIL', 'support@moon-well.online'), + 'effective_date' => env('MOONWELL_LEGAL_EFFECTIVE_DATE', '25 июля 2026 года'), + ], + 'client' => [ 'disk' => env('MOONWELL_CLIENT_DISK', 's3'), 'object_key' => env('MOONWELL_CLIENT_OBJECT_KEY', 'moonwell_launcher_setup.exe'), diff --git a/resources/css/moonwell.css b/resources/css/moonwell.css index 2c89a6e..4e74f68 100644 --- a/resources/css/moonwell.css +++ b/resources/css/moonwell.css @@ -1597,3 +1597,1751 @@ body[data-particles="false"] #particles { display: none; } /* ------------------------- utility ------------------------- */ .ornament-svg { display: inline-block; vertical-align: middle; } + +/* ------------------------- Inertia portal / legal ------------------------- */ +.portal-shell { + position: relative; + z-index: 1; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.portal-main { flex: 1; } + +.portal-header { + position: sticky; + top: 0; + z-index: 50; + border-bottom: 1px solid var(--panel-border); + background: rgba(7, 10, 24, 0.86); + backdrop-filter: blur(18px); +} + +.portal-header__inner { + min-height: 76px; + display: flex; + align-items: center; + gap: 32px; +} + +.portal-brand { + display: flex; + align-items: center; + gap: 13px; + min-width: 0; +} + +.portal-brand__mark { + display: grid; + place-items: center; + flex: 0 0 auto; + color: var(--moon); + filter: drop-shadow(0 0 16px rgba(127, 184, 212, 0.3)); +} + +.portal-brand strong, +.portal-brand small, +.portal-account strong, +.portal-account small { + display: block; +} + +.portal-brand strong { + font-family: var(--f-display); + font-size: 16px; + font-weight: 500; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--ivory); +} + +.portal-brand small { + max-width: 280px; + overflow: hidden; + color: var(--text-mute); + font-size: 10px; + line-height: 1.35; + text-overflow: ellipsis; + white-space: nowrap; +} + +.portal-nav { + display: flex; + align-items: center; + gap: 24px; + margin-left: auto; +} + +.portal-nav a { + position: relative; + padding: 28px 0 25px; + font-family: var(--f-mono); + font-size: 10px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-mute); + transition: color var(--t-fast); +} + +.portal-nav a::after { + position: absolute; + right: 0; + bottom: -1px; + left: 0; + height: 1px; + content: ""; + background: var(--moon); + transform: scaleX(0); + transition: transform var(--t-fast); +} + +.portal-nav a:hover, +.portal-nav a.active { color: var(--ivory); } +.portal-nav a:hover::after, +.portal-nav a.active::after { transform: scaleX(1); } + +.portal-account { + display: flex; + align-items: center; + gap: 14px; + padding-left: 20px; + border-left: 1px solid var(--panel-border); +} + +.portal-account strong { + max-width: 130px; + overflow: hidden; + font-family: var(--f-display); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.1em; + color: var(--ivory); + text-overflow: ellipsis; +} + +.portal-account small { + color: var(--text-mute); + font-size: 10px; +} + +.portal-logout { + padding: 9px 12px; + border: 1px solid var(--panel-border-strong); + border-radius: var(--radius); + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; + transition: border-color var(--t-fast), color var(--t-fast); +} + +.portal-logout:hover { border-color: var(--moon); color: var(--moon); } + +.portal-menu-toggle { + display: none; + width: 42px; + height: 42px; + margin-left: auto; + border: 1px solid var(--panel-border-strong); + border-radius: var(--radius); + flex-direction: column; + align-items: center; + justify-content: center; + gap: 5px; +} + +.portal-menu-toggle span { + width: 18px; + height: 1px; + background: var(--ivory); +} + +.portal-mobile-menu { + display: none; + border-top: 1px solid var(--panel-border); + background: rgba(7, 10, 24, 0.98); +} + +.portal-mobile-menu.open { display: block; } + +.portal-mobile-menu nav { + display: flex; + flex-direction: column; + padding-top: 10px; + padding-bottom: 18px; +} + +.portal-mobile-menu a, +.portal-mobile-menu button { + padding: 15px 0; + border-bottom: 1px solid var(--panel-border); + text-align: left; + font-family: var(--f-display); + font-size: 17px; + letter-spacing: 0.04em; + color: var(--ivory); +} + +.portal-alerts { padding-top: 24px; } + +.portal-alert { + padding: 14px 18px; + border: 1px solid; + border-radius: var(--radius-lg); + font-size: 14px; + backdrop-filter: blur(10px); +} + +.portal-alert + .portal-alert { margin-top: 10px; } +.portal-alert--success { + border-color: rgba(110, 220, 142, 0.28); + color: #9ae8ae; + background: rgba(110, 220, 142, 0.08); +} +.portal-alert--error { + border-color: rgba(232, 167, 107, 0.3); + color: #e8a76b; + background: rgba(232, 167, 107, 0.08); +} + +.portal-welcome { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 40px; + padding-top: clamp(64px, 8vw, 110px); + padding-bottom: 48px; +} + +.portal-welcome h1 { + max-width: 850px; + margin: 18px 0 14px; + font-size: clamp(38px, 5.5vw, 70px); +} + +.portal-welcome h1 em { + font-family: var(--f-serif); + font-weight: 400; + color: var(--moon); + text-transform: none; +} + +.portal-welcome .lead { max-width: 680px; margin: 0; } + +.portal-welcome__status { + display: flex; + align-items: center; + gap: 12px; + min-width: max-content; + padding: 16px 20px; + border: 1px solid var(--panel-border); + border-radius: var(--radius-lg); + background: rgba(10, 13, 31, 0.58); +} + +.portal-status-dot { + width: 9px; + height: 9px; + border-radius: 50%; + background: #6edc8e; + box-shadow: 0 0 12px rgba(110, 220, 142, 0.8); +} + +.portal-status-dot.danger { + background: #e8a76b; + box-shadow: 0 0 12px rgba(232, 167, 107, 0.8); +} + +.portal-welcome__status small, +.portal-welcome__status strong { display: block; } +.portal-welcome__status small { + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--text-mute); +} +.portal-welcome__status strong { + margin-top: 2px; + font-family: var(--f-display); + font-size: 13px; + font-weight: 500; + color: var(--ivory); +} + +.portal-dashboard { + display: grid; + grid-template-columns: 210px minmax(0, 1fr); + gap: clamp(32px, 5vw, 70px); + align-items: start; +} + +.portal-sidebar { + position: sticky; + top: 104px; + display: flex; + flex-direction: column; + padding: 18px 0; + border-top: 1px solid var(--panel-border); + border-bottom: 1px solid var(--panel-border); +} + +.portal-sidebar__label, +.portal-card-label { + margin-bottom: 10px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text-mute); +} + +.portal-sidebar button { + display: flex; + align-items: center; + gap: 13px; + padding: 13px 4px; + text-align: left; + font-family: var(--f-display); + font-size: 14px; + letter-spacing: 0.04em; + color: var(--text-mute); + transition: color var(--t-fast), padding var(--t-fast); +} + +.portal-sidebar button span { + font-family: var(--f-mono); + font-size: 9px; + color: var(--text-dim); +} + +.portal-sidebar button:hover, +.portal-sidebar button.active { + padding-left: 10px; + color: var(--ivory); +} + +.portal-sidebar button.active span { color: var(--moon); } + +.portal-sidebar__balance { + margin-top: 20px; + padding: 18px; + border: 1px solid var(--panel-border); + border-radius: var(--radius-lg); + background: linear-gradient(180deg, rgba(127, 184, 212, 0.06), rgba(91, 58, 138, 0.06)); +} + +.portal-sidebar__balance > * { display: block; } +.portal-sidebar__balance small, +.portal-sidebar__balance span { + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-mute); +} +.portal-sidebar__balance strong { + margin: 4px 0 2px; + font-family: var(--f-display); + font-size: 24px; + font-weight: 500; + color: var(--ivory); +} + +.portal-content { min-width: 0; } + +.portal-section { + scroll-margin-top: 90px; + padding: 38px 0 72px; + border-top: 1px solid var(--panel-border); +} + +.portal-section:first-child { padding-top: 38px; } + +.portal-section__head { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 24px; + margin-bottom: 28px; +} + +.portal-section__head .eyebrow { margin-bottom: 10px; } +.portal-section__head h2 { + margin: 0; + font-size: clamp(32px, 4vw, 46px); +} + +.portal-section__count { + font-family: var(--f-mono); + font-size: 10px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-mute); +} + +.portal-grid { + display: grid; + gap: 18px; +} + +.portal-grid--account { grid-template-columns: 1.25fr 0.75fr; } +.portal-grid--wallet { grid-template-columns: 0.9fr 1.1fr; } + +.portal-card { + position: relative; + padding: 28px; + overflow: hidden; + border: 1px solid var(--panel-border); + border-radius: var(--radius-lg); + background: + radial-gradient(ellipse at 100% 0%, rgba(127, 184, 212, 0.055), transparent 52%), + linear-gradient(180deg, rgba(16, 20, 42, 0.78), rgba(10, 13, 31, 0.84)); + box-shadow: 0 32px 90px -48px rgba(0, 0, 0, 0.9); + backdrop-filter: blur(10px); +} + +.portal-stats { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 26px 28px; + margin: 0; +} + +.portal-stats dt, +.portal-stats dd { margin: 0; } +.portal-stats dt { + margin-bottom: 5px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-mute); +} +.portal-stats dd { + overflow-wrap: anywhere; + font-family: var(--f-display); + font-size: 15px; + letter-spacing: 0.03em; + color: var(--ivory); +} + +.portal-launcher-card { + display: flex; + flex-direction: column; + align-items: flex-start; +} +.portal-launcher-card h3 { + margin: 16px 0 10px; + font-size: 30px; +} +.portal-launcher-card p { + flex: 1; + margin: 0 0 24px; + font-family: var(--f-serif); + font-size: 17px; + line-height: 1.55; + color: var(--text-soft); +} +.portal-launcher-card .btn { width: 100%; padding-right: 18px; padding-left: 18px; } + +.portal-file-note { + width: 100%; + margin-top: 13px; + overflow: hidden; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.08em; + text-align: center; + text-overflow: ellipsis; + color: var(--text-mute); +} + +.portal-small-button { padding: 11px 18px; } + +.portal-character-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.portal-character-card { + padding: 24px; + transition: transform var(--t-fast), border-color var(--t-fast); +} + +.portal-character-card:hover { + border-color: var(--panel-border-strong); + transform: translateY(-2px); +} + +.portal-character-card__head { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 26px; +} + +.portal-character-level { + display: grid; + width: 42px; + height: 42px; + place-items: center; + border: 1px solid var(--panel-border-strong); + transform: rotate(45deg); + font-family: var(--f-display); + font-size: 14px; + color: var(--moon); +} + +.portal-character-level > span { transform: rotate(-45deg); } + +.portal-online { + display: flex; + align-items: center; + gap: 7px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--text-dim); +} +.portal-online::before { + width: 5px; + height: 5px; + border-radius: 50%; + content: ""; + background: var(--text-dim); +} +.portal-online.online { color: #6edc8e; } +.portal-online.online::before { + background: #6edc8e; + box-shadow: 0 0 8px #6edc8e; +} + +.portal-character-card h3 { margin: 0 0 4px; font-size: 28px; } +.portal-character-card > p { margin: 0 0 22px; color: var(--text-mute); font-size: 13px; } +.portal-character-card dl { + display: grid; + gap: 10px; + margin: 0; + padding-top: 16px; + border-top: 1px solid var(--panel-border); +} +.portal-character-card dl div { + display: flex; + justify-content: space-between; + gap: 14px; +} +.portal-character-card dt, +.portal-character-card dd { margin: 0; font-size: 12px; } +.portal-character-card dt { color: var(--text-mute); } +.portal-character-card dd { text-align: right; color: var(--ivory); } + +.portal-empty { + padding: 54px 30px; + text-align: center; +} +.portal-empty__moon { + display: block; + margin-bottom: 12px; + font-family: var(--f-display); + font-size: 52px; + color: var(--moon); +} +.portal-empty h3 { margin: 0; font-size: 30px; } +.portal-empty p { + margin: 12px 0 24px; + font-family: var(--f-serif); + font-size: 18px; + color: var(--text-soft); +} + +.portal-wallet-card { + background: + radial-gradient(ellipse at 50% 0%, rgba(176, 136, 238, 0.14), transparent 58%), + linear-gradient(180deg, rgba(20, 24, 50, 0.88), rgba(10, 13, 31, 0.88)); +} + +.portal-balance { + margin-top: 8px; + font-family: var(--f-display); + font-size: clamp(44px, 6vw, 70px); + line-height: 1; + color: var(--ivory); +} + +.portal-wallet-card > p { + margin: 8px 0 24px; + font-family: var(--f-serif); + font-size: 18px; + color: var(--moon); +} + +.portal-deposit { + display: grid; + grid-template-columns: 1fr auto; + gap: 12px; + align-items: end; + padding-top: 22px; + border-top: 1px solid var(--panel-border); +} + +.portal-deposit .portal-field { margin: 0; } +.portal-deposit .btn { padding: 15px 20px; } + +.portal-rate { + display: flex; + justify-content: space-between; + gap: 16px; + margin-top: 14px; + font-family: var(--f-mono); + font-size: 9px; + line-height: 1.5; + color: var(--text-mute); +} + +.portal-history-card { padding: 28px 30px; } +.portal-history { margin-top: 7px; } +.portal-history__row { + display: flex; + justify-content: space-between; + align-items: center; + gap: 18px; + padding: 14px 0; + border-bottom: 1px solid var(--panel-border); +} +.portal-history__row:last-child { border-bottom: 0; } +.portal-history__row > span, +.portal-history__row small { display: block; } +.portal-history__row strong { font-size: 13px; font-weight: 500; color: var(--text-soft); } +.portal-history__row small { + margin-top: 3px; + font-family: var(--f-mono); + font-size: 9px; + color: var(--text-mute); +} +.portal-history__row > strong { + font-family: var(--f-mono); + white-space: nowrap; + color: var(--text-soft); +} +.portal-history__row > strong.positive { color: #6edc8e; } +.portal-history-empty { + display: grid; + min-height: 220px; + place-items: center; + font-family: var(--f-serif); + font-size: 18px; + color: var(--text-mute); +} + +.portal-security-card { + display: grid; + grid-template-columns: 0.85fr 1.15fr; + gap: clamp(32px, 6vw, 80px); + padding: clamp(28px, 5vw, 52px); +} +.portal-security-card__copy h3 { margin: 0 0 14px; font-size: 30px; } +.portal-security-card__copy p { + margin: 0; + font-family: var(--f-serif); + font-size: 18px; + line-height: 1.55; + color: var(--text-soft); +} +.portal-security-card__copy ul { + display: grid; + gap: 10px; + margin: 24px 0 0; + padding: 0; + list-style: none; + font-size: 13px; + color: var(--text-mute); +} +.portal-security-card__copy li::before { + margin-right: 10px; + content: "✦"; + color: var(--moon); +} + +.portal-password-form { display: grid; gap: 17px; } + +.portal-field { + display: block; + margin: 22px 0; +} + +.portal-field > span { + display: block; + margin-bottom: 8px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--ivory); +} + +.portal-field input { + width: 100%; + min-height: 50px; + padding: 13px 15px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + outline: 0; + background: rgba(7, 10, 24, 0.7); + font: inherit; + color: var(--ivory); + transition: border-color var(--t-fast), box-shadow var(--t-fast); +} + +.portal-field input:focus { + border-color: var(--moon); + box-shadow: 0 0 0 3px rgba(127, 184, 212, 0.1); +} + +.portal-field.has-error input { border-color: rgba(232, 167, 107, 0.58); } +.portal-field small, +.portal-field-error { + display: block; + margin-top: 7px; + font-family: var(--f-mono); + font-size: 10px; + line-height: 1.5; + color: #e8a76b; +} + +.portal-submit { width: 100%; } +.portal-submit:disabled { opacity: 0.55; cursor: wait; } + +.portal-auth { + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: clamp(48px, 8vw, 110px); + align-items: center; + min-height: calc(100vh - 180px); + padding-top: clamp(60px, 8vw, 110px); + padding-bottom: clamp(70px, 9vw, 130px); +} + +.portal-auth__intro h1 { + max-width: 760px; + margin: 20px 0 22px; + font-size: clamp(42px, 6.5vw, 82px); +} +.portal-auth__intro > p { max-width: 620px; margin: 0; } + +.portal-feature-list { + display: grid; + gap: 0; + max-width: 590px; + margin-top: 40px; + border-top: 1px solid var(--panel-border); +} +.portal-feature-list div { + display: grid; + grid-template-columns: 38px 1fr; + align-items: center; + gap: 12px; + padding: 14px 0; + border-bottom: 1px solid var(--panel-border); +} +.portal-feature-list span { + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.15em; + color: var(--moon); +} +.portal-feature-list p { margin: 0; color: var(--text-soft); font-size: 13px; } + +.portal-form-card { + padding: clamp(30px, 4vw, 46px); + background: + radial-gradient(ellipse at 50% 0%, rgba(127, 184, 212, 0.1), transparent 58%), + linear-gradient(180deg, rgba(16, 20, 42, 0.92), rgba(10, 13, 31, 0.92)); +} +.portal-form-card__mark { + margin-bottom: 24px; + color: var(--moon); + filter: drop-shadow(0 0 18px rgba(127, 184, 212, 0.36)); +} +.portal-form-card h2 { margin: 14px 0 8px; font-size: 34px; } +.portal-form-card > p { margin: 0 0 24px; color: var(--text-mute); font-size: 13px; } +.portal-back-link { + display: block; + margin-top: 20px; + text-align: center; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-mute); + transition: color var(--t-fast); +} +.portal-back-link:hover { color: var(--moon); } + +.portal-footer { + margin-top: 30px; + border-top: 1px solid var(--panel-border); + background: rgba(7, 10, 24, 0.45); +} +.portal-footer__inner { + min-height: 100px; + display: flex; + align-items: center; + gap: 30px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--text-dim); +} +.portal-footer__brand { + display: flex; + align-items: center; + gap: 9px; + color: var(--ivory); +} +.portal-footer__brand svg { color: var(--moon); } +.portal-footer nav { + display: flex; + gap: 22px; + margin-left: auto; +} +.portal-footer a { transition: color var(--t-fast); } +.portal-footer a:hover { color: var(--moon); } + +.legal-page { + padding-top: clamp(70px, 9vw, 125px); + padding-bottom: clamp(80px, 10vw, 140px); +} +.legal-hero { + max-width: 940px; + padding: 0 0 64px; +} +.legal-hero h1 { + margin: 20px 0 24px; + font-size: clamp(46px, 8vw, 96px); +} +.legal-hero .lead { max-width: 760px; } +.legal-effective { + display: inline-flex; + gap: 16px; + margin-top: 30px; + padding: 12px 16px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: rgba(10, 13, 31, 0.5); + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; +} +.legal-effective span { color: var(--text-mute); } +.legal-effective strong { font-weight: 500; color: var(--moon); } + +.legal-layout { + display: grid; + grid-template-columns: 230px minmax(0, 800px); + gap: clamp(36px, 6vw, 82px); + align-items: start; +} +.legal-toc { + position: sticky; + top: 110px; + display: flex; + flex-direction: column; + padding: 18px 0; + border-top: 1px solid var(--panel-border); + border-bottom: 1px solid var(--panel-border); +} +.legal-toc > span { + margin-bottom: 10px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--text-mute); +} +.legal-toc a { + display: grid; + grid-template-columns: 28px 1fr; + gap: 8px; + padding: 8px 0; + font-family: var(--f-serif); + font-size: 15px; + line-height: 1.25; + color: var(--text-soft); + transition: color var(--t-fast); +} +.legal-toc a:hover { color: var(--moon); } +.legal-toc small { + padding-top: 3px; + font-family: var(--f-mono); + font-size: 8px; + color: var(--text-dim); +} +.legal-document { padding: clamp(30px, 5vw, 58px); } +.legal-document section { + scroll-margin-top: 100px; + padding: 42px 0; + border-bottom: 1px solid var(--panel-border); +} +.legal-document section:first-child { padding-top: 0; } +.legal-document section:last-child { padding-bottom: 0; border-bottom: 0; } +.legal-section-number { + display: block; + margin-bottom: 9px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.18em; + color: var(--moon); +} +.legal-document h2 { margin: 0 0 20px; font-size: clamp(27px, 4vw, 38px); } +.legal-document p, +.legal-document li { + font-family: var(--f-serif); + font-size: 18px; + line-height: 1.7; + color: var(--text-soft); +} +.legal-document p { margin: 0 0 15px; } +.legal-document ul { + display: grid; + gap: 10px; + margin: 18px 0; + padding-left: 24px; +} +.legal-document li::marker { color: var(--moon); } +.legal-document .legal-note { + margin-top: 22px; + padding: 17px 19px; + border-left: 2px solid var(--moon); + background: rgba(127, 184, 212, 0.06); + font-size: 16px; + color: var(--ivory); +} + +@media (max-width: 1080px) { + .portal-nav { gap: 16px; } + .portal-nav a { font-size: 9px; } + .portal-brand small { display: none; } + .portal-dashboard { grid-template-columns: 180px minmax(0, 1fr); gap: 34px; } + .portal-character-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 880px) { + .portal-nav--desktop { display: none; } + .portal-menu-toggle { display: flex; } + .portal-header__inner { min-height: 66px; } + .portal-welcome { + align-items: flex-start; + flex-direction: column; + padding-top: 60px; + } + .portal-dashboard { display: block; } + .portal-sidebar { + position: static; + flex-direction: row; + overflow-x: auto; + gap: 5px; + padding: 10px 0; + scrollbar-width: none; + } + .portal-sidebar::-webkit-scrollbar { display: none; } + .portal-sidebar__label, + .portal-sidebar__balance { display: none; } + .portal-sidebar button { + flex: 0 0 auto; + padding: 10px 13px; + border: 1px solid transparent; + } + .portal-sidebar button:hover, + .portal-sidebar button.active { + padding-left: 13px; + border-color: var(--panel-border); + background: rgba(127, 184, 212, 0.05); + } + .portal-grid--account, + .portal-grid--wallet, + .portal-security-card, + .portal-auth { grid-template-columns: 1fr; } + .portal-auth { min-height: auto; } + .portal-auth__intro h1 { max-width: 680px; } + .legal-layout { grid-template-columns: 1fr; } + .legal-toc { position: static; } +} + +@media (max-width: 640px) { + .portal-header__inner { gap: 12px; } + .portal-brand strong { font-size: 14px; } + .portal-welcome { gap: 24px; padding-bottom: 32px; } + .portal-welcome h1 { font-size: 37px; } + .portal-welcome__status { width: 100%; } + .portal-section { padding: 30px 0 54px; } + .portal-section__head { align-items: flex-start; flex-direction: column; } + .portal-stats, + .portal-character-grid { grid-template-columns: 1fr; } + .portal-card { padding: 23px; } + .portal-launcher-card h3 { font-size: 27px; } + .portal-deposit { grid-template-columns: 1fr; } + .portal-deposit .btn { width: 100%; } + .portal-rate { flex-direction: column; gap: 4px; } + .portal-security-card { padding: 25px; } + .portal-auth { padding-top: 54px; } + .portal-auth__intro h1 { font-size: 42px; } + .portal-footer__inner { + align-items: flex-start; + flex-direction: column; + padding-top: 28px; + padding-bottom: 28px; + } + .portal-footer nav { flex-direction: column; gap: 9px; margin-left: 0; } + .footer-bottom { align-items: flex-start; flex-direction: column; gap: 12px; } + .legal-hero { padding-bottom: 42px; } + .legal-hero h1 { font-size: 43px; overflow-wrap: anywhere; } + .legal-effective { align-items: flex-start; flex-direction: column; gap: 5px; } + .legal-document { padding: 25px; } + .legal-document section { padding: 34px 0; } + .legal-document p, + .legal-document li { font-size: 17px; } +} + +/* ------------------------- Inertia admin ------------------------- */ +.admin-bar { + position: sticky; + top: 76px; + z-index: 35; + border-bottom: 1px solid var(--panel-border); + background: rgba(10, 13, 31, 0.9); + backdrop-filter: blur(16px); +} + +.admin-bar__inner { + display: flex; + align-items: center; + min-height: 54px; + overflow-x: auto; + scrollbar-width: none; +} +.admin-bar__inner::-webkit-scrollbar { display: none; } +.admin-bar a { + position: relative; + display: flex; + flex: 0 0 auto; + align-items: center; + gap: 8px; + padding: 17px 20px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-mute); + transition: color var(--t-fast), background var(--t-fast); +} +.admin-bar a:first-child { margin-left: -20px; } +.admin-bar a::after { + position: absolute; + right: 20px; + bottom: -1px; + left: 20px; + height: 1px; + content: ""; + background: var(--moon); + transform: scaleX(0); + transition: transform var(--t-fast); +} +.admin-bar a:hover, +.admin-bar a.active { + color: var(--ivory); + background: rgba(127, 184, 212, 0.035); +} +.admin-bar a.active::after { transform: scaleX(1); } +.admin-bar small { font-size: 8px; color: var(--moon); } + +.admin-hero { + display: flex; + justify-content: space-between; + align-items: flex-end; + gap: 40px; + padding-top: clamp(60px, 8vw, 105px); + padding-bottom: clamp(42px, 6vw, 70px); +} +.admin-hero h1 { + max-width: 920px; + margin: 19px 0 15px; + font-size: clamp(43px, 7vw, 88px); +} +.admin-hero .lead { max-width: 760px; margin: 0; } +.admin-hero__badge { + min-width: 150px; + padding: 20px 24px; + border: 1px solid var(--panel-border); + border-radius: var(--radius-lg); + text-align: right; + background: linear-gradient(180deg, rgba(127, 184, 212, 0.06), rgba(91, 58, 138, 0.04)); +} +.admin-hero__badge small, +.admin-hero__badge strong { display: block; } +.admin-hero__badge small { + font-family: var(--f-mono); + font-size: 8px; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--text-mute); +} +.admin-hero__badge strong { + margin-top: 4px; + font-family: var(--f-display); + font-size: 38px; + font-weight: 500; + line-height: 1; + color: var(--ivory); +} + +.admin-page { padding-bottom: 40px; } +.admin-section { + padding: 44px 0 76px; + border-top: 1px solid var(--panel-border); +} +.admin-section__head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 40px; + margin-bottom: 30px; +} +.admin-section__head .eyebrow { margin-bottom: 9px; } +.admin-section__head h2, +.admin-panel > h2 { + margin: 0; + font-size: clamp(31px, 4vw, 45px); +} +.admin-section__head > p { + max-width: 500px; + margin: 0; + text-align: right; + font-family: var(--f-serif); + font-size: 17px; + line-height: 1.5; + color: var(--text-mute); +} + +.admin-stat-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-bottom: 20px; +} +.admin-stat { padding: 22px 24px; } +.admin-stat span, +.admin-stat small, +.admin-stat strong { display: block; } +.admin-stat span { + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.17em; + text-transform: uppercase; + color: var(--text-mute); +} +.admin-stat strong { + margin-top: 5px; + font-family: var(--f-display); + font-size: 34px; + font-weight: 500; + color: var(--ivory); +} +.admin-stat small { color: var(--text-dim); } + +.admin-split { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 18px; + align-items: start; +} +.admin-split--form { grid-template-columns: 1.05fr 0.95fr; } +.admin-panel { padding: clamp(26px, 4vw, 42px); } +.admin-panel > h2 { margin: 10px 0 20px; } +.admin-panel > h3 { margin: 8px 0 16px; font-size: 29px; } +.admin-panel > p { + margin: 0 0 22px; + font-family: var(--f-serif); + font-size: 17px; + line-height: 1.55; + color: var(--text-soft); +} +.admin-kicker { + display: block; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.19em; + text-transform: uppercase; + color: var(--moon); +} + +.admin-inline-form { + display: grid; + grid-template-columns: 1fr auto; + align-items: end; + gap: 12px; + margin-top: 24px; +} +.admin-inline-form .admin-field { margin: 0; } +.admin-inline-form .btn { min-height: 50px; padding: 14px 20px; } + +.admin-form { margin-top: 24px; } +.admin-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 15px; +} +.admin-field { + display: block; + margin: 0 0 17px; + min-width: 0; +} +.admin-field--wide { grid-column: 1 / -1; } +.admin-field > span { + display: block; + margin-bottom: 7px; + font-family: var(--f-sans); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.13em; + text-transform: uppercase; + color: var(--ivory); +} +.admin-field input, +.admin-field select, +.admin-field textarea, +.admin-search input { + width: 100%; + min-height: 48px; + padding: 12px 14px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + outline: 0; + background: rgba(7, 10, 24, 0.72); + font-family: var(--f-sans); + font-size: 14px; + color: var(--ivory); + transition: border-color var(--t-fast), box-shadow var(--t-fast); +} +.admin-field textarea { + min-height: 92px; + resize: vertical; + line-height: 1.55; +} +.admin-field select[multiple] { min-height: 90px; } +.admin-field input[type="file"] { padding: 10px; } +.admin-field input:focus, +.admin-field select:focus, +.admin-field textarea:focus, +.admin-search input:focus { + border-color: var(--moon); + box-shadow: 0 0 0 3px rgba(127, 184, 212, 0.09); +} +.admin-field small, +.admin-field em { + display: block; + margin-top: 6px; + font-family: var(--f-sans); + font-size: 11px; + font-style: normal; + line-height: 1.45; +} +.admin-field small { color: #e8a76b; } +.admin-field em { color: var(--text-mute); } + +.admin-check-row { + display: flex; + flex-wrap: wrap; + gap: 14px 28px; +} +.admin-check { + position: relative; + display: flex; + align-items: center; + gap: 10px; + margin: 4px 0 18px; + color: var(--text-soft); + font-size: 13px; + cursor: pointer; +} +.admin-check input { + position: absolute; + opacity: 0; + pointer-events: none; +} +.admin-check__box { + position: relative; + width: 18px; + height: 18px; + flex: 0 0 auto; + border: 1px solid var(--panel-border-strong); + border-radius: 2px; + background: rgba(7, 10, 24, 0.7); +} +.admin-check input:checked + .admin-check__box { + border-color: var(--moon); + background: var(--moon); +} +.admin-check input:checked + .admin-check__box::after { + position: absolute; + top: 2px; + left: 5px; + width: 5px; + height: 9px; + content: ""; + border-right: 2px solid var(--bg-0); + border-bottom: 2px solid var(--bg-0); + transform: rotate(45deg); +} +.admin-submit { min-width: 190px; padding: 14px 24px; } +.admin-submit:disabled { opacity: 0.55; cursor: wait; } +.admin-form-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; +} +.admin-danger-button { + padding: 13px 21px; + border-color: rgba(232, 167, 107, 0.22); + color: #e8a76b; +} +.admin-danger-button:hover { + border-color: rgba(232, 167, 107, 0.55); + background: rgba(232, 167, 107, 0.06); +} + +.admin-compact-list { + max-height: 370px; + margin-top: 18px; + overflow-y: auto; +} +.admin-compact-list > div { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: 18px; + padding: 14px 0; + border-bottom: 1px solid var(--panel-border); +} +.admin-compact-list > div:last-child { border-bottom: 0; } +.admin-compact-list span, +.admin-compact-list strong, +.admin-compact-list small { display: block; } +.admin-compact-list > div > span:last-child { text-align: right; } +.admin-compact-list strong { font-size: 12px; font-weight: 500; color: var(--text-soft); } +.admin-compact-list small { margin-top: 3px; font-size: 10px; color: var(--text-mute); } +.admin-compact-list .admin-code { + font-family: var(--f-mono); + letter-spacing: 0.08em; + color: var(--ivory); +} +.admin-positive { color: #6edc8e !important; } +.admin-empty-inline { + padding: 28px 0; + font-family: var(--f-serif); + font-size: 17px; + color: var(--text-mute); +} + +.admin-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; + margin-bottom: 20px; +} +.admin-tabs { + display: inline-flex; + padding: 3px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: rgba(7, 10, 24, 0.54); +} +.admin-tabs a { + padding: 10px 15px; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-mute); +} +.admin-tabs a.active { background: var(--ivory); color: var(--bg-0); } +.admin-tabs span { margin-left: 5px; opacity: 0.7; } +.admin-search { + display: flex; + flex: 1; + justify-content: flex-end; + gap: 9px; +} +.admin-search input { max-width: 350px; } +.admin-search .btn { min-height: 48px; padding: 12px 18px; } + +.admin-account-list, +.admin-ledger, +.admin-editor-list { + display: grid; + gap: 12px; +} +.admin-account { padding: 0; } +.admin-account__main { + display: grid; + grid-template-columns: minmax(230px, 1.1fr) minmax(340px, 1.6fr) minmax(250px, 1fr) auto; + align-items: end; + gap: 22px; + padding: 22px 24px; +} +.admin-account__identity { + display: flex; + align-items: center; + gap: 12px; + align-self: center; +} +.admin-account__identity > span:last-child, +.admin-account__identity strong, +.admin-account__identity small { display: block; min-width: 0; } +.admin-account__identity strong { + overflow: hidden; + font-family: var(--f-display); + font-size: 15px; + font-weight: 500; + letter-spacing: 0.06em; + text-overflow: ellipsis; + color: var(--ivory); +} +.admin-account__identity small { + margin-top: 4px; + overflow: hidden; + font-size: 10px; + text-overflow: ellipsis; + color: var(--text-mute); +} +.admin-account__status { + width: 8px; + height: 8px; + flex: 0 0 auto; + border-radius: 50%; + background: #6edc8e; + box-shadow: 0 0 9px rgba(110, 220, 142, 0.7); +} +.admin-account__status.danger { + background: #e8a76b; + box-shadow: 0 0 9px rgba(232, 167, 107, 0.7); +} +.admin-account__stats { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + margin: 0; +} +.admin-account__stats dt, +.admin-account__stats dd { margin: 0; } +.admin-account__stats dt { + margin-bottom: 3px; + font-family: var(--f-mono); + font-size: 7px; + letter-spacing: 0.11em; + text-transform: uppercase; + color: var(--text-dim); +} +.admin-account__stats dd { + font-size: 10px; + line-height: 1.35; + color: var(--text-soft); +} +.admin-access-form { + display: flex; + align-items: end; + gap: 8px; +} +.admin-access-form .admin-field { min-width: 150px; margin: 0; } +.admin-access-form .admin-field > span { font-size: 8px; } +.admin-access-form select { min-height: 42px; padding: 9px 10px; font-size: 11px; } +.admin-access-form .btn { min-height: 42px; padding: 10px 13px; font-size: 9px; } +.admin-expand { + padding: 10px 0; + font-family: var(--f-mono); + font-size: 8px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--moon); +} +.admin-account__characters { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 9px; + padding: 18px 24px 22px; + border-top: 1px solid var(--panel-border); + background: rgba(7, 10, 24, 0.38); +} +.admin-account__characters > div { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); +} +.admin-account__characters span, +.admin-account__characters strong, +.admin-account__characters small { display: block; } +.admin-account__characters > div > span:last-child { text-align: right; } +.admin-account__characters strong { font-size: 11px; color: var(--ivory); } +.admin-account__characters small { margin-top: 2px; font-size: 9px; color: var(--text-mute); } +.admin-account__characters p { grid-column: 1 / -1; color: var(--text-mute); } + +.admin-rules > div { + display: grid; + grid-template-columns: 32px 1fr; + gap: 12px; + padding: 16px 0; + border-top: 1px solid var(--panel-border); +} +.admin-rules > div > span { + padding-top: 3px; + font-family: var(--f-mono); + font-size: 9px; + color: var(--moon); +} +.admin-rules > div > p { + margin: 0; + font-family: var(--f-serif); + font-size: 17px; + color: var(--text-soft); +} + +.admin-ledger__row { + display: grid; + grid-template-columns: minmax(180px, 0.85fr) minmax(300px, 1.5fr) minmax(160px, 0.65fr); + align-items: center; + gap: 24px; + padding: 19px 22px; +} +.admin-ledger__row > div, +.admin-ledger__row strong, +.admin-ledger__row small { display: block; min-width: 0; } +.admin-ledger__row strong { font-size: 12px; font-weight: 500; color: var(--text-soft); } +.admin-ledger__row small { + margin-top: 3px; + overflow-wrap: anywhere; + font-size: 9px; + color: var(--text-mute); +} +.admin-ledger__amount { text-align: right; } +.admin-ledger__amount > strong { + font-family: var(--f-mono); + font-size: 14px; + color: var(--text-soft); +} +.admin-ledger__amount > strong.positive { color: #6edc8e; } + +.admin-notice { + margin-bottom: 20px; + padding: 15px 18px; + border: 1px solid var(--panel-border); + border-left: 2px solid var(--arcane-2); + border-radius: var(--radius); + background: rgba(91, 58, 138, 0.07); + font-size: 12px; + line-height: 1.55; + color: var(--text-mute); +} +.admin-notice code { color: var(--moon); } + +.admin-editor { padding: 0; overflow: clip; } +.admin-editor > summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 25px; + padding: 21px 24px; + list-style: none; + cursor: pointer; + transition: background var(--t-fast); +} +.admin-editor > summary::-webkit-details-marker { display: none; } +.admin-editor > summary:hover { background: rgba(127, 184, 212, 0.035); } +.admin-editor > summary::before { + width: 7px; + height: 7px; + flex: 0 0 auto; + content: ""; + border-right: 1px solid var(--moon); + border-bottom: 1px solid var(--moon); + transform: rotate(-45deg); + transition: transform var(--t-fast); +} +.admin-editor[open] > summary::before { transform: rotate(45deg); } +.admin-editor > summary > span:first-of-type { flex: 1; min-width: 0; } +.admin-editor > summary strong, +.admin-editor > summary small { display: block; } +.admin-editor > summary strong { + overflow: hidden; + font-family: var(--f-display); + font-size: 14px; + font-weight: 500; + letter-spacing: 0.04em; + text-overflow: ellipsis; + color: var(--ivory); +} +.admin-editor > summary small { margin-top: 4px; font-size: 10px; color: var(--text-mute); } +.admin-editor__body { + padding: 26px 30px 30px; + border-top: 1px solid var(--panel-border); + background: rgba(7, 10, 24, 0.32); +} +.admin-editor__body > .admin-danger-button { margin-top: 16px; } +.admin-pill { + flex: 0 0 auto; + padding: 7px 10px; + border: 1px solid rgba(127, 184, 212, 0.25); + border-radius: 999px; + font-family: var(--f-mono); + font-size: 8px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--moon); + background: rgba(127, 184, 212, 0.06); +} +.admin-pill.muted { border-color: var(--panel-border); color: var(--text-mute); background: transparent; } +.admin-preline { white-space: pre-line; } + +.admin-subform { + margin: 8px 0 18px; + border-top: 1px solid var(--panel-border); + border-bottom: 1px solid var(--panel-border); +} +.admin-subform summary { + padding: 14px 0; + font-family: var(--f-mono); + font-size: 9px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--moon); + cursor: pointer; +} +.admin-rewards { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 18px; + padding: 10px 0; +} +.admin-rewards > div { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 10px; +} + +.admin-create-panel { + max-width: 880px; + margin-top: 18px; +} +.admin-news-image { + width: min(100%, 380px); + max-height: 240px; + margin-bottom: 8px; + border: 1px solid var(--panel-border); + border-radius: var(--radius-lg); + object-fit: cover; +} +.admin-upload input::file-selector-button { + margin-right: 12px; + padding: 6px 10px; + border: 1px solid var(--panel-border-strong); + border-radius: var(--radius); + background: rgba(127, 184, 212, 0.06); + color: var(--ivory); + cursor: pointer; +} +.admin-code-block, +.admin-feed-preview { + display: block; + width: 100%; + margin: 18px 0 0; + padding: 17px; + overflow-x: auto; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: rgba(7, 10, 24, 0.65); + font-family: var(--f-mono); + font-size: 10px; + line-height: 1.65; + white-space: pre-wrap; + overflow-wrap: anywhere; + color: var(--text-soft); +} +.admin-code-block { color: var(--moon); } +.admin-feed-preview { max-height: 360px; overflow-y: auto; } + +.admin-empty { + padding: 50px 30px; + text-align: center; + color: var(--text-mute); +} +.admin-empty > span { font-family: var(--f-display); font-size: 42px; color: var(--moon); } +.admin-empty h3 { margin: 7px 0; font-size: 28px; } +.admin-empty p { margin: 0; font-family: var(--f-serif); font-size: 18px; } + +@media (max-width: 1180px) { + .admin-account__main { + grid-template-columns: minmax(220px, 1fr) minmax(300px, 1.4fr) minmax(250px, 1fr); + } + .admin-expand { grid-column: 1 / -1; justify-self: start; padding: 0; } +} + +@media (max-width: 960px) { + .admin-bar { top: 66px; } + .admin-split, + .admin-split--form { grid-template-columns: 1fr; } + .admin-toolbar, + .admin-section__head { align-items: flex-start; flex-direction: column; } + .admin-section__head > p { text-align: left; } + .admin-search { width: 100%; justify-content: flex-start; } + .admin-account__main { grid-template-columns: 1fr 1fr; align-items: center; } + .admin-account__stats { grid-column: 1 / -1; grid-row: 2; } + .admin-access-form { grid-column: 1 / -1; } + .admin-expand { grid-column: auto; justify-self: end; } + .admin-account__characters { grid-template-columns: repeat(2, minmax(0, 1fr)); } +} + +@media (max-width: 700px) { + .admin-bar a:first-child { margin-left: 0; } + .admin-bar a { padding: 15px 13px; } + .admin-bar a::after { right: 13px; left: 13px; } + .admin-hero { align-items: flex-start; flex-direction: column; padding-top: 55px; } + .admin-hero h1 { font-size: 43px; overflow-wrap: anywhere; } + .admin-hero__badge { width: 100%; text-align: left; } + .admin-section { padding: 36px 0 58px; } + .admin-stat-grid { grid-template-columns: 1fr; } + .admin-form-grid, + .admin-rewards { grid-template-columns: 1fr; } + .admin-inline-form { grid-template-columns: 1fr; } + .admin-inline-form .btn, + .admin-submit { width: 100%; } + .admin-toolbar { align-items: stretch; } + .admin-tabs { width: 100%; } + .admin-tabs a { flex: 1; text-align: center; } + .admin-search { flex-wrap: wrap; } + .admin-search input { max-width: none; flex: 1 1 100%; } + .admin-account__main { grid-template-columns: 1fr; } + .admin-account__stats, + .admin-access-form, + .admin-expand { grid-column: auto; grid-row: auto; } + .admin-account__stats { grid-template-columns: 1fr 1fr; } + .admin-access-form { flex-direction: column; align-items: stretch; } + .admin-access-form .admin-field { width: 100%; } + .admin-expand { justify-self: start; } + .admin-account__characters { grid-template-columns: 1fr; } + .admin-ledger__row { grid-template-columns: 1fr; gap: 14px; } + .admin-ledger__amount { text-align: left; } + .admin-editor > summary { align-items: flex-start; flex-wrap: wrap; gap: 13px; padding: 18px; } + .admin-editor > summary::before { margin-top: 5px; } + .admin-editor__body { padding: 22px 18px; } + .admin-rewards > div { grid-template-columns: 1fr 1fr; } + .admin-form-actions { align-items: stretch; flex-direction: column; } + .admin-form-actions .btn { width: 100%; } +} diff --git a/resources/js/Components/LegalDocument.vue b/resources/js/Components/LegalDocument.vue new file mode 100644 index 0000000..84be0ae --- /dev/null +++ b/resources/js/Components/LegalDocument.vue @@ -0,0 +1,45 @@ + + + diff --git a/resources/js/Components/SiteFooter.vue b/resources/js/Components/SiteFooter.vue new file mode 100644 index 0000000..84ef30a --- /dev/null +++ b/resources/js/Components/SiteFooter.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/js/Components/SiteHeader.vue b/resources/js/Components/SiteHeader.vue new file mode 100644 index 0000000..5282af4 --- /dev/null +++ b/resources/js/Components/SiteHeader.vue @@ -0,0 +1,90 @@ + + + diff --git a/resources/js/Layouts/AdminLayout.vue b/resources/js/Layouts/AdminLayout.vue new file mode 100644 index 0000000..aba918f --- /dev/null +++ b/resources/js/Layouts/AdminLayout.vue @@ -0,0 +1,39 @@ + + + diff --git a/resources/js/Layouts/PortalLayout.vue b/resources/js/Layouts/PortalLayout.vue new file mode 100644 index 0000000..a2508f7 --- /dev/null +++ b/resources/js/Layouts/PortalLayout.vue @@ -0,0 +1,43 @@ + + + diff --git a/resources/js/Pages/Admin/Accounts.vue b/resources/js/Pages/Admin/Accounts.vue new file mode 100644 index 0000000..6885af7 --- /dev/null +++ b/resources/js/Pages/Admin/Accounts.vue @@ -0,0 +1,206 @@ + + + diff --git a/resources/js/Pages/Admin/Balances.vue b/resources/js/Pages/Admin/Balances.vue new file mode 100644 index 0000000..4345c5b --- /dev/null +++ b/resources/js/Pages/Admin/Balances.vue @@ -0,0 +1,133 @@ + + + diff --git a/resources/js/Pages/Admin/Components/CategoryForm.vue b/resources/js/Pages/Admin/Components/CategoryForm.vue new file mode 100644 index 0000000..acd77da --- /dev/null +++ b/resources/js/Pages/Admin/Components/CategoryForm.vue @@ -0,0 +1,69 @@ + + + diff --git a/resources/js/Pages/Admin/Components/NewsForm.vue b/resources/js/Pages/Admin/Components/NewsForm.vue new file mode 100644 index 0000000..1eb2452 --- /dev/null +++ b/resources/js/Pages/Admin/Components/NewsForm.vue @@ -0,0 +1,90 @@ + + + diff --git a/resources/js/Pages/Admin/Components/ProductForm.vue b/resources/js/Pages/Admin/Components/ProductForm.vue new file mode 100644 index 0000000..9744f1f --- /dev/null +++ b/resources/js/Pages/Admin/Components/ProductForm.vue @@ -0,0 +1,149 @@ + + + diff --git a/resources/js/Pages/Admin/Components/StatCards.vue b/resources/js/Pages/Admin/Components/StatCards.vue new file mode 100644 index 0000000..ce4dd01 --- /dev/null +++ b/resources/js/Pages/Admin/Components/StatCards.vue @@ -0,0 +1,15 @@ + + + diff --git a/resources/js/Pages/Admin/LoginScreenNews.vue b/resources/js/Pages/Admin/LoginScreenNews.vue new file mode 100644 index 0000000..4244269 --- /dev/null +++ b/resources/js/Pages/Admin/LoginScreenNews.vue @@ -0,0 +1,120 @@ + + + diff --git a/resources/js/Pages/Admin/News.vue b/resources/js/Pages/Admin/News.vue new file mode 100644 index 0000000..43de723 --- /dev/null +++ b/resources/js/Pages/Admin/News.vue @@ -0,0 +1,96 @@ + + + diff --git a/resources/js/Pages/Admin/Shop.vue b/resources/js/Pages/Admin/Shop.vue new file mode 100644 index 0000000..86d3ddb --- /dev/null +++ b/resources/js/Pages/Admin/Shop.vue @@ -0,0 +1,122 @@ + + + diff --git a/resources/js/Pages/Auth/Login.vue b/resources/js/Pages/Auth/Login.vue new file mode 100644 index 0000000..368b18f --- /dev/null +++ b/resources/js/Pages/Auth/Login.vue @@ -0,0 +1,95 @@ + + + diff --git a/resources/js/Pages/Cabinet/Index.vue b/resources/js/Pages/Cabinet/Index.vue new file mode 100644 index 0000000..1aaf868 --- /dev/null +++ b/resources/js/Pages/Cabinet/Index.vue @@ -0,0 +1,263 @@ + + + diff --git a/resources/js/Pages/Landing/Components/Footer.vue b/resources/js/Pages/Landing/Components/Footer.vue index 43a9be3..d7bd456 100644 --- a/resources/js/Pages/Landing/Components/Footer.vue +++ b/resources/js/Pages/Landing/Components/Footer.vue @@ -1,7 +1,10 @@ diff --git a/resources/js/Pages/Legal/Offer.vue b/resources/js/Pages/Legal/Offer.vue new file mode 100644 index 0000000..b2c274c --- /dev/null +++ b/resources/js/Pages/Legal/Offer.vue @@ -0,0 +1,109 @@ + + + diff --git a/resources/js/Pages/Legal/Privacy.vue b/resources/js/Pages/Legal/Privacy.vue new file mode 100644 index 0000000..699bee0 --- /dev/null +++ b/resources/js/Pages/Legal/Privacy.vue @@ -0,0 +1,117 @@ + + + diff --git a/resources/views/admin/accounts/index.blade.php b/resources/views/admin/accounts/index.blade.php deleted file mode 100644 index b0e500e..0000000 --- a/resources/views/admin/accounts/index.blade.php +++ /dev/null @@ -1,178 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Инвайты -

Управление инвайт-кодами

-

{{ config('moonwell.registration.require_invite_code') ? 'Регистрация требует действующий инвайт-код.' : 'Регистрация сейчас открыта без инвайт-кодов.' }}

-
- -
-
-

Создать инвайт-коды

- -
-
-
Всего
-
{{ $inviteStats['total'] }}
-
-
-
Доступно
-
{{ $inviteStats['available'] }}
-
-
-
Использовано
-
{{ $inviteStats['redeemed'] }}
-
-
- -
- @csrf - - - - -
-
- -
-

Последние инвайт-коды

- - @if ($inviteCodes->isNotEmpty()) -
- @foreach ($inviteCodes as $invite) -
-
- {{ $invite->code }} - - Создал: {{ $invite->created_by_username ?: '—' }} - @if ($invite->created_at) - · {{ $invite->created_at->format('d.m.Y H:i') }} - @endif - -
-
- {{ $invite->redeemed_at ? 'Использован' : 'Свободен' }} - - {{ $invite->redeemed_by_username ?: 'ещё не использован' }} - -
-
- @endforeach -
- @else -
Инвайт-коды ещё не создавались.
- @endif -
-
-
- -
-
- Админка -

Аккаунты и персонажи

-

Здесь видны все игровые аккаунты, их персонажи и текущий уровень доступа.

-
- -
- @foreach ($accountTabs as $tabKey => $tab) - - {{ $tab['label'] }} · {{ $tab['count'] }} - - @endforeach -
- -
- - - - @if ($search !== '') - Сбросить - @endif -
-
- -
- -
-@endsection diff --git a/resources/views/admin/balances/index.blade.php b/resources/views/admin/balances/index.blade.php deleted file mode 100644 index 599725c..0000000 --- a/resources/views/admin/balances/index.blade.php +++ /dev/null @@ -1,102 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Финансы -

Слёзы Элуны

-

Ручные операции сохраняются в общем журнале с администратором и причиной изменения.

-
- -
-
-

Изменить количество

-
- @csrf - - - - - - - - - - -
-
- -
-

Правила операций

-

Количество нельзя сделать отрицательным. Каждая корректировка содержит ID администратора, логин, причину и итоговое значение.

-

Операции необратимы в журнале: ошибочное начисление исправляется отдельным списанием.

-
-
-
- -
-
- Аудит -

Последние транзакции

-
- -
- - - @if ($search !== '') - Сбросить - @endif -
- - -
-@endsection diff --git a/resources/views/admin/login-screen-news/index.blade.php b/resources/views/admin/login-screen-news/index.blade.php deleted file mode 100644 index d32e785..0000000 --- a/resources/views/admin/login-screen-news/index.blade.php +++ /dev/null @@ -1,165 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Админка -

Новости логин-экрана WoW

-

Публикуй raw-текст с WoW-разметкой, а сайт сам отдаст готовый `SERVERALERT`-фид по открытой ссылке для клиента 3.3.5a.

-
- -
-
-

Публичная ссылка для клиента

-

Эту ссылку можно вставить в логин-скрин клиента. Ответ возвращается как plain text и доступен без авторизации.

- -
- Открыть фид - {{ $feedUrl }} -
- -
-
-
Всего записей
-
{{ $newsStats['total'] }}
-
-
-
Опубликовано
-
{{ $newsStats['published'] }}
-
-
-
Черновиков
-
{{ $newsStats['draft'] }}
-
-
-
- -
-

Итоговый ответ сервера

-

Это превью полного ответа, который получит клиент по публичному URL.

- -
{{ $feedPreview }}
-
-
-
- -
-
- Новая запись -

Добавить новость

-

Каждая запись становится отдельным блоком в общем фиде. Префикс `SERVERALERT:` добавляется автоматически.

- -
- @csrf - - - - - - - - - - -
-
- -
- Шаблон -

Пример WoW-блока

-

Если нужно, можно вставлять уже готовые строки почти в том же виде, как у тебя в примере с GitHub raw.

- -
|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
-
-
- -
-
- Текущие записи -

Редактирование и публикация

-

Опубликованные записи попадают в фид по порядку `sort_order`. Черновики остаются только в админке.

-
- - -
-@endsection diff --git a/resources/views/admin/news/index.blade.php b/resources/views/admin/news/index.blade.php deleted file mode 100644 index 4145b7f..0000000 --- a/resources/views/admin/news/index.blade.php +++ /dev/null @@ -1,154 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Админка -

Новости

-

Новости отображаются в лаунчере. Картинка загружается в S3-хранилище.

-
- -
-
-

Статистика

- -
-
-
Всего
-
{{ $newsStats['total'] }}
-
-
-
Опубликовано
-
{{ $newsStats['published'] }}
-
-
-
Черновиков
-
{{ $newsStats['draft'] }}
-
-
-
-
-
- -
-
- Новая запись -

Добавить новость

- -
- @csrf - - - - - - - - - - - - -
-
-
- -
-
- Текущие записи -

Редактирование

-
- - -
-@endsection diff --git a/resources/views/admin/shop/index.blade.php b/resources/views/admin/shop/index.blade.php deleted file mode 100644 index 8f91608..0000000 --- a/resources/views/admin/shop/index.blade.php +++ /dev/null @@ -1,76 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Игровой сервер -

Магазин

-

Изменения записываются прямо в базу store. После сохранения перезагрузи данные магазина командой игрового модуля или перезапусти worldserver.

-
- -
-
-

Новая категория

-
- @csrf - @include('admin.shop.partials.category-fields', ['category' => null]) - -
-
- -
-

Новый товар или услуга

- @if ($categories->isEmpty()) -

Сначала создай категорию.

- @else -
- @csrf - @include('admin.shop.partials.product-fields', ['product' => null]) - -
- @endif -
-
-
- -
-
Навигация магазина

Категории

- -
- -
-
Контент магазина

Товары и услуги

- -
-@endsection diff --git a/resources/views/admin/shop/partials/category-fields.blade.php b/resources/views/admin/shop/partials/category-fields.blade.php deleted file mode 100644 index 54086ff..0000000 --- a/resources/views/admin/shop/partials/category-fields.blade.php +++ /dev/null @@ -1,6 +0,0 @@ -@php($category = $category ?? null) - - - - - diff --git a/resources/views/admin/shop/partials/product-fields.blade.php b/resources/views/admin/shop/partials/product-fields.blade.php deleted file mode 100644 index fe203a2..0000000 --- a/resources/views/admin/shop/partials/product-fields.blade.php +++ /dev/null @@ -1,28 +0,0 @@ -@php($product = $product ?? null) -@php($selectedCategories = $product?->categories?->pluck('id')->all() ?? []) - - - - - - - -
- - -
-
- - - -
-
- @foreach (range(1, 8) as $slot) - - - @endforeach -
-
- - -
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php deleted file mode 100644 index 52b6a69..0000000 --- a/resources/views/auth/login.blade.php +++ /dev/null @@ -1,61 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Личный кабинет -

Вход в игровой аккаунт

-

- Используй логин и пароль от игрового аккаунта. После входа будут доступны персонажи, - смена пароля и ссылка на клиент игры. -

- -
    -
  • Просмотр всех персонажей аккаунта
  • -
  • Смена пароля без ручных команд
  • -
  • Быстрый переход к клиенту игры
  • -
-
- - -
-@endsection diff --git a/resources/views/cabinet/index.blade.php b/resources/views/cabinet/index.blade.php deleted file mode 100644 index 72b1d75..0000000 --- a/resources/views/cabinet/index.blade.php +++ /dev/null @@ -1,190 +0,0 @@ -@extends('layouts.portal') - -@section('portal-content') -
-
- Личный кабинет -

{{ $user->username }}

-

Здесь можно посмотреть персонажей, сменить пароль и скачать лаунчер.

-
- -
-
-

Данные аккаунта

- -
-
-
Логин
-
{{ $user->username }}
-
-
-
Email
-
{{ $user->email !== '' ? $user->email : '—' }}
-
-
-
Уровень доступа
-
{{ $user->accessLabel() }}
-
-
-
Регистрация
-
{{ $user->joinedAtLabel() }}
-
-
-
Последний вход
-
{{ $user->lastLoginLabel() }}
-
-
-
Статус
-
{{ $user->locked ? 'Заблокирован' : 'Активен' }}
-
-
- - @if ($user->canAccessAdminPanel()) - Перейти в админку - @endif -
- -
-

Лаунчер

-

Скачай лаунчер для автоматической загрузки и обновления клиента игры.

- -
- Скачать лаунчер - Файл: {{ $clientFilename }} -
-
-
-
- -
-
- Слёзы Элуны -

{{ number_format((float) $balance, 2, ',', ' ') }}

-

Пополняй счёт и используй Слёзы Элуны для покупок в игре.

- -
- @csrf - - - 1 ₽ = {{ $tearsPerRuble }} Слёз Элуны - От {{ $minimumDeposit }} до {{ number_format((float) $maximumDeposit, 0, ',', ' ') }} ₽ -
-
- -
- История -

Последние операции

- @forelse ($balanceTransactions as $transaction) -
-
- {{ $transaction->type === 'deposit' ? 'Пополнение' : 'Покупка в игре' }} - {{ $transaction->created_at->format('d.m.Y H:i') }} -
- - {{ (float) $transaction->amount >= 0 ? '+' : '' }}{{ number_format((float) $transaction->amount, 2, ',', ' ') }} - -
- @empty -
Операций пока нет.
- @endforelse -
-
- -
-
- Безопасность -

Сменить пароль

-

После обновления новый пароль сразу начнёт действовать для входа в игру и кабинет.

- -
- @csrf - - - - - - - - -
-
- -
- Персонажи -

Краткая сводка

-

Всего персонажей: {{ $characters->count() }}. Ниже показан полный список по этому аккаунту.

- - @if ($characters->isNotEmpty()) -
- @foreach ($characters as $character) - {{ $character['name'] }} · {{ $character['class'] }} · {{ $character['level'] }} - @endforeach -
- @else -
Персонажей пока нет.
- @endif -
-
- -
-
- Мои персонажи -

Все персонажи аккаунта

-

Здесь отображаются игровые персонажи, связанные с текущим аккаунтом.

-
- -
- @forelse ($characters as $character) -
-
-
-

{{ $character['name'] }}

-

{{ $character['race'] }} · {{ $character['class'] }}

-
- - {{ $character['online'] ? 'Онлайн' : 'Оффлайн' }} -
- -
-
-
Уровень
-
{{ $character['level'] }}
-
-
-
Пол
-
{{ $character['gender'] }}
-
-
-
Золото
-
{{ $character['money'] }}
-
-
-
Наработано
-
{{ $character['played'] }}
-
-
-
- @empty -
-

Персонажей пока нет

-

Создай персонажа в игре, и он появится здесь автоматически.

-
- @endforelse -
-
-@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php deleted file mode 100644 index 6c81679..0000000 --- a/resources/views/layouts/app.blade.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - {{ config('moonwell.realm.server_name') }} — регистрация WoW аккаунта - - - - - - - - @yield('content') - @include('partials.yandex-metrika') - - diff --git a/resources/views/layouts/portal.blade.php b/resources/views/layouts/portal.blade.php deleted file mode 100644 index e83b2dc..0000000 --- a/resources/views/layouts/portal.blade.php +++ /dev/null @@ -1,55 +0,0 @@ -@extends('layouts.app') - -@section('content') - @php($portalUser = auth()->user()) - -
-
- - - - {{ config('moonwell.realm.server_name') }} - {{ config('moonwell.realm.tagline') }} - - - - - -
- @if ($portalUser) - {{ $portalUser->username }} · {{ $portalUser->accessLabel() }} - -
- @csrf - -
- @else - Войти - @endif -
-
- - @if (session('status')) -
{{ session('status') }}
- @endif - - @if (session('error')) -
{{ session('error') }}
- @endif - -
- @yield('portal-content') -
-
-@endsection diff --git a/routes/web.php b/routes/web.php index d33a0c2..68d6deb 100644 --- a/routes/web.php +++ b/routes/web.php @@ -9,13 +9,16 @@ use App\Http\Controllers\Admin\ShopController; use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\CabinetController; use App\Http\Controllers\LandingController; +use App\Http\Controllers\LegalController; use App\Http\Controllers\PaymentController; use App\Http\Controllers\WowLoginNewsFeedController; use Illuminate\Support\Facades\Route; Route::get('/', [LandingController::class, 'index'])->name('landing.index'); +Route::get('/public-offer', [LegalController::class, 'offer'])->name('legal.offer'); +Route::get('/privacy-policy', [LegalController::class, 'privacy'])->name('legal.privacy'); Route::get('/launcher/login-news.txt', [WowLoginNewsFeedController::class, 'show'])->name('launcher.login-news.show'); -Route::get('/appcast.xml', static fn() => Storage::disk('public')->get('appcast.xml')); +Route::get('/appcast.xml', static fn () => Storage::disk('public')->get('appcast.xml')); Route::post('/register', [LandingController::class, 'register']) ->middleware('throttle:8,1') ->name('game-account.store'); diff --git a/tests/Feature/AdminAccountsFeatureTest.php b/tests/Feature/AdminAccountsFeatureTest.php index 8884a7c..d32f505 100644 --- a/tests/Feature/AdminAccountsFeatureTest.php +++ b/tests/Feature/AdminAccountsFeatureTest.php @@ -6,6 +6,7 @@ use App\Models\GameAccountUser; use App\Services\AzerothCoreAccountService; use App\Services\InviteCodeService; use Illuminate\Support\Collection; +use Inertia\Testing\AssertableInertia as Assert; use Mockery\MockInterface; use Tests\TestCase; @@ -64,7 +65,7 @@ class AdminAccountsFeatureTest extends TestCase $this->mock(InviteCodeService::class, function (MockInterface $mock): void { $mock->shouldReceive('latest') ->once() - ->andReturn(new Collection()); + ->andReturn(new Collection); $mock->shouldReceive('stats') ->once() @@ -74,11 +75,14 @@ class AdminAccountsFeatureTest extends TestCase $response = $this->actingAs($this->makeUser(3))->get(route('admin.accounts.index')); $response->assertOk(); - $response->assertSee('Stormmage'); - $response->assertSee('Аккаунты и персонажи'); - $response->assertSee('Управление инвайт-кодами'); - $response->assertSee('Игроки · 1'); - $response->assertSee('Боты · 1'); + $response->assertInertia(fn (Assert $page) => $page + ->component('Admin/Accounts', false) + ->where('accounts.0.username', 'ADMIN') + ->where('accounts.0.characters.0.name', 'Stormmage') + ->where('accountTabs.players.count', 1) + ->where('accountTabs.bots.count', 1) + ->etc() + ); } public function test_admin_can_open_bots_tab(): void @@ -104,7 +108,7 @@ class AdminAccountsFeatureTest extends TestCase 'online' => false, 'joined_at' => '14.03.2026 01:00', 'last_login_at' => '14.03.2026 01:30', - 'characters' => new Collection(), + 'characters' => new Collection, ], ])); }); @@ -112,7 +116,7 @@ class AdminAccountsFeatureTest extends TestCase $this->mock(InviteCodeService::class, function (MockInterface $mock): void { $mock->shouldReceive('latest') ->once() - ->andReturn(new Collection()); + ->andReturn(new Collection); $mock->shouldReceive('stats') ->once() @@ -122,8 +126,13 @@ class AdminAccountsFeatureTest extends TestCase $response = $this->actingAs($this->makeUser(3))->get(route('admin.accounts.index', ['tab' => 'bots'])); $response->assertOk(); - $response->assertSee('RNDBOT0007'); - $response->assertSee('Боты · 2'); + $response->assertInertia(fn (Assert $page) => $page + ->component('Admin/Accounts', false) + ->where('activeTab', 'bots') + ->where('accounts.0.username', 'RNDBOT0007') + ->where('accountTabs.bots.count', 2) + ->etc() + ); } public function test_admin_can_update_access_level(): void diff --git a/tests/Feature/AdminBalancesFeatureTest.php b/tests/Feature/AdminBalancesFeatureTest.php index 3ca5062..5a306fb 100644 --- a/tests/Feature/AdminBalancesFeatureTest.php +++ b/tests/Feature/AdminBalancesFeatureTest.php @@ -7,6 +7,7 @@ use App\Models\GameAccountUser; use App\Services\AzerothCoreAccountService; use App\Services\BalanceService; use Illuminate\Support\Collection; +use Inertia\Testing\AssertableInertia as Assert; use Mockery\MockInterface; use Tests\TestCase; @@ -20,14 +21,18 @@ class AdminBalancesFeatureTest extends TestCase public function test_admin_can_view_transaction_log(): void { $this->mock(BalanceService::class, function (MockInterface $mock): void { - $mock->shouldReceive('adminTransactions')->once()->with(null)->andReturn(new Collection()); + $mock->shouldReceive('adminTransactions')->once()->with(null)->andReturn(new Collection); }); $this->actingAs($this->makeUser(3)) ->get(route('admin.balances.index')) ->assertOk() - ->assertSee('Слёзы Элуны') - ->assertSee('Последние транзакции'); + ->assertInertia(fn (Assert $page) => $page + ->component('Admin/Balances', false) + ->has('transactions', 0) + ->where('adjustUrl', route('admin.balances.adjust')) + ->etc() + ); } public function test_admin_can_credit_account_balance(): void diff --git a/tests/Feature/AdminNewsFeatureTest.php b/tests/Feature/AdminNewsFeatureTest.php new file mode 100644 index 0000000..6b3e259 --- /dev/null +++ b/tests/Feature/AdminNewsFeatureTest.php @@ -0,0 +1,60 @@ +create([ + 'title' => 'MoonWell Update', + 'body' => 'Подробности обновления.', + 'sort_order' => 5, + 'is_published' => true, + 'created_by_username' => 'ADMIN', + ]); + + $this->actingAs($this->admin()) + ->get(route('admin.news.index')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('Admin/News', false) + ->where('newsItems.0.title', 'MoonWell Update') + ->where('newsItems.0.is_published', true) + ->where('newsStats.published', 1) + ->where('storeUrl', route('admin.news.store')) + ->etc() + ); + } + + public function test_regular_player_cannot_open_news_admin(): void + { + $this->actingAs($this->admin(0)) + ->get(route('admin.news.index')) + ->assertForbidden(); + } + + private function admin(int $gmLevel = 3): GameAccountUser + { + return new GameAccountUser( + 1, + 'ADMIN', + 'ADMIN@EXAMPLE.COM', + 'ADMIN@EXAMPLE.COM', + $gmLevel, + false, + null, + null, + str_repeat('AA', 32), + str_repeat('BB', 32), + ); + } +} diff --git a/tests/Feature/AdminShopFeatureTest.php b/tests/Feature/AdminShopFeatureTest.php index e84bbc9..45f3990 100644 --- a/tests/Feature/AdminShopFeatureTest.php +++ b/tests/Feature/AdminShopFeatureTest.php @@ -6,6 +6,7 @@ use App\Models\GameAccountUser; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; class AdminShopFeatureTest extends TestCase @@ -66,7 +67,16 @@ class AdminShopFeatureTest extends TestCase $this->assertDatabaseHas('store_services', ['name' => 'Спектральный тигр', 'price' => 3000], 'store'); $this->assertDatabaseHas('store_category_service_link', ['category' => $categoryId], 'store'); - $this->actingAs($admin)->get(route('admin.shop.index'))->assertOk()->assertSee('Спектральный тигр'); + $this->actingAs($admin) + ->get(route('admin.shop.index')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('Admin/Shop', false) + ->where('categories.0.name', 'Маунты') + ->where('products.0.name', 'Спектральный тигр') + ->where('products.0.category_ids.0', $categoryId) + ->etc() + ); } public function test_regular_player_cannot_manage_store(): void diff --git a/tests/Feature/CabinetFeatureTest.php b/tests/Feature/CabinetFeatureTest.php index 29034d8..3840f5a 100644 --- a/tests/Feature/CabinetFeatureTest.php +++ b/tests/Feature/CabinetFeatureTest.php @@ -7,6 +7,7 @@ use App\Services\AzerothCoreAccountService; use App\Services\BalanceService; use App\Services\GameClientDownloadService; use Illuminate\Support\Collection; +use Inertia\Testing\AssertableInertia as Assert; use Mockery\MockInterface; use Tests\TestCase; @@ -48,9 +49,15 @@ class CabinetFeatureTest extends TestCase $response = $this->actingAs($user)->get(route('cabinet.index')); $response->assertOk(); - $response->assertSee('Arthion'); - $response->assertSee('1 250,00'); - $response->assertSee('Скачать лаунчер'); + $response->assertInertia(fn (Assert $page) => $page + ->component('Cabinet/Index', false) + ->where('account.username', 'PLAYERONE') + ->where('wallet.formatted_amount', '1 250,00') + ->where('characters.0.name', 'Arthion') + ->has('characters', 1) + ->where('urls.client', route('cabinet.client')) + ->etc() + ); } public function test_authenticated_user_can_change_password(): void diff --git a/tests/Feature/InertiaPublicPagesFeatureTest.php b/tests/Feature/InertiaPublicPagesFeatureTest.php new file mode 100644 index 0000000..1c1f37a --- /dev/null +++ b/tests/Feature/InertiaPublicPagesFeatureTest.php @@ -0,0 +1,50 @@ +get(route('login')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('Auth/Login', false) + ->where('auth.authenticated', false) + ->where('urls.login_store', route('login.store')) + ->etc() + ); + } + + public function test_hidden_legal_pages_return_not_found(): void + { + $this->get(route('legal.offer'))->assertNotFound(); + $this->get(route('legal.privacy'))->assertNotFound(); + } + + public function test_legal_pages_can_be_published_by_configuration(): void + { + config(['moonwell.legal.published' => true]); + + $this->get(route('legal.offer')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('Legal/Offer', false) + ->where('legal.operator_name', config('moonwell.legal.operator_name')) + ->where('urls.privacy', route('legal.privacy')) + ->etc() + ); + + $this->get(route('legal.privacy')) + ->assertOk() + ->assertInertia(fn (Assert $page) => $page + ->component('Legal/Privacy', false) + ->where('legal.contact_email', config('moonwell.legal.contact_email')) + ->where('urls.offer', route('legal.offer')) + ->etc() + ); + } +} diff --git a/tests/Feature/LandingPageTest.php b/tests/Feature/LandingPageTest.php index c9deafc..d5d44f1 100644 --- a/tests/Feature/LandingPageTest.php +++ b/tests/Feature/LandingPageTest.php @@ -2,6 +2,7 @@ namespace Tests\Feature; +use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; class LandingPageTest extends TestCase @@ -11,8 +12,14 @@ class LandingPageTest extends TestCase $response = $this->get(route('landing.index')); $response->assertOk(); - $response->assertSee('Создать игровой аккаунт'); - $response->assertSee(config('moonwell.realm.server_name')); + $response->assertInertia(fn (Assert $page) => $page + ->component('Landing/Main', false) + ->where('realm.server_name', config('moonwell.realm.server_name')) + ->where('legalUrls.published', false) + ->where('legalUrls.offer', null) + ->where('legalUrls.privacy', null) + ->etc() + ); } public function test_landing_page_uses_https_assets_behind_proxy(): void @@ -26,6 +33,6 @@ class LandingPageTest extends TestCase ])->get(route('landing.index')); $response->assertOk(); - $response->assertSee('href="https://moon-well.online/site.css"', false); + $response->assertSee('href="https://moon-well.online/build/assets/', false); } } diff --git a/tests/Feature/LoginScreenNewsFeatureTest.php b/tests/Feature/LoginScreenNewsFeatureTest.php index dce518e..bba98c7 100644 --- a/tests/Feature/LoginScreenNewsFeatureTest.php +++ b/tests/Feature/LoginScreenNewsFeatureTest.php @@ -6,6 +6,7 @@ use App\Models\GameAccountUser; use App\Models\LoginScreenNews; use App\Services\LoginScreenNewsService; use Illuminate\Foundation\Testing\RefreshDatabase; +use Inertia\Testing\AssertableInertia as Assert; use Tests\TestCase; class LoginScreenNewsFeatureTest extends TestCase @@ -86,9 +87,13 @@ class LoginScreenNewsFeatureTest extends TestCase $response = $this->actingAs($this->makeUser(3))->get(route('admin.login-screen-news.index')); $response->assertOk(); - $response->assertSee('Новости логин-экрана WoW'); - $response->assertSee('Wrath Update'); - $response->assertSee(route('launcher.login-news.show'), false); + $response->assertInertia(fn (Assert $page) => $page + ->component('Admin/LoginScreenNews', false) + ->where('newsItems.0.title', 'Wrath Update') + ->where('feedUrl', route('launcher.login-news.show')) + ->where('newsStats.published', 1) + ->etc() + ); } public function test_admin_page_always_renders_feed_link_with_http_scheme(): void @@ -102,7 +107,11 @@ class LoginScreenNewsFeatureTest extends TestCase ])->actingAs($this->makeUser(3))->get(route('admin.login-screen-news.index')); $response->assertOk(); - $response->assertSee('http://moon-well.online/launcher/login-news.txt', false); + $response->assertInertia(fn (Assert $page) => $page + ->component('Admin/LoginScreenNews', false) + ->where('feedUrl', 'http://moon-well.online/launcher/login-news.txt') + ->etc() + ); } public function test_admin_can_create_update_and_delete_news_item(): void