264 lines
14 KiB
Vue
264 lines
14 KiB
Vue
<script setup>
|
||
import { Head, useForm, usePage } from '@inertiajs/vue3';
|
||
import { computed, ref } from 'vue';
|
||
import PortalLayout from '../../Layouts/PortalLayout.vue';
|
||
import { arrowSvg } from '../Landing/Components/symbols';
|
||
|
||
const props = defineProps({
|
||
site: { type: Object, required: true },
|
||
auth: { type: Object, required: true },
|
||
urls: { type: Object, required: true },
|
||
csrfToken: { type: String, required: true },
|
||
flash: { type: Object, default: () => ({}) },
|
||
account: { type: Object, required: true },
|
||
characters: { type: Array, default: () => [] },
|
||
client: { type: Object, required: true },
|
||
wallet: { type: Object, required: true },
|
||
});
|
||
|
||
const page = usePage();
|
||
const sharedErrors = computed(() => page.props.errors || {});
|
||
const activeSection = ref('overview');
|
||
const passwordForm = useForm({
|
||
current_password: '',
|
||
password: '',
|
||
password_confirmation: '',
|
||
});
|
||
|
||
const sections = [
|
||
{ id: 'overview', label: 'Обзор', number: '01' },
|
||
{ id: 'characters', label: 'Персонажи', number: '02' },
|
||
{ id: 'wallet', label: 'Слёзы Элуны', number: '03' },
|
||
{ id: 'security', label: 'Безопасность', number: '04' },
|
||
];
|
||
|
||
function goTo(id) {
|
||
activeSection.value = id;
|
||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
|
||
function updatePassword() {
|
||
passwordForm.post(props.urls.password, {
|
||
preserveScroll: true,
|
||
onSuccess: () => passwordForm.reset(),
|
||
onError: () => {
|
||
passwordForm.reset('password', 'password_confirmation');
|
||
goTo('security');
|
||
},
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<Head>
|
||
<title>Личный кабинет — {{ site.name }}</title>
|
||
<meta name="description" content="Персонажи, баланс и настройки игрового аккаунта MoonWell." />
|
||
</Head>
|
||
|
||
<PortalLayout :site="site" :auth="auth" :urls="urls" :flash="flash">
|
||
<section class="portal-welcome wrap">
|
||
<div>
|
||
<span class="eyebrow">Личный кабинет</span>
|
||
<h1 class="h-display">С возвращением, <em>{{ account.username }}</em></h1>
|
||
<p class="lead">Твой аккаунт, герои и всё необходимое для следующего путешествия.</p>
|
||
</div>
|
||
<div class="portal-welcome__status">
|
||
<span :class="['portal-status-dot', { danger: account.locked }]"></span>
|
||
<span>
|
||
<small>Статус аккаунта</small>
|
||
<strong>{{ account.status_label }}</strong>
|
||
</span>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="portal-dashboard wrap">
|
||
<aside class="portal-sidebar">
|
||
<span class="portal-sidebar__label">Навигация</span>
|
||
<button
|
||
v-for="section in sections"
|
||
:key="section.id"
|
||
:class="{ active: activeSection === section.id }"
|
||
type="button"
|
||
@click="goTo(section.id)"
|
||
>
|
||
<span>{{ section.number }}</span>
|
||
{{ section.label }}
|
||
</button>
|
||
<div class="portal-sidebar__balance">
|
||
<small>Баланс</small>
|
||
<strong>{{ wallet.formatted_amount }}</strong>
|
||
<span>Слёз Элуны</span>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="portal-content">
|
||
<section id="overview" class="portal-section">
|
||
<div class="portal-section__head">
|
||
<div>
|
||
<span class="eyebrow">Обзор</span>
|
||
<h2 class="h-serif">Данные аккаунта</h2>
|
||
</div>
|
||
<a v-if="auth.is_admin" class="btn btn-ghost portal-small-button" :href="urls.admin">Открыть админку</a>
|
||
</div>
|
||
|
||
<div class="portal-grid portal-grid--account">
|
||
<article class="portal-card portal-account-card corners">
|
||
<dl class="portal-stats">
|
||
<div><dt>Логин</dt><dd>{{ account.username }}</dd></div>
|
||
<div><dt>Email</dt><dd>{{ account.email }}</dd></div>
|
||
<div><dt>Уровень доступа</dt><dd>{{ account.access_label }}</dd></div>
|
||
<div><dt>Регистрация</dt><dd>{{ account.joined_at }}</dd></div>
|
||
<div><dt>Последний вход</dt><dd>{{ account.last_login_at }}</dd></div>
|
||
<div><dt>Персонажей</dt><dd>{{ characters.length }}</dd></div>
|
||
</dl>
|
||
</article>
|
||
|
||
<article class="portal-card portal-launcher-card corners">
|
||
<span class="eyebrow">Лаунчер</span>
|
||
<h3 class="h-serif">Врата в Азерот</h3>
|
||
<p>Лаунчер автоматически скачает клиент, настроит realmlist и установит обновления.</p>
|
||
<a class="btn btn-primary" :href="urls.client">
|
||
Скачать лаунчер <span v-html="arrowSvg"></span>
|
||
</a>
|
||
<span class="portal-file-note">{{ client.filename }}</span>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="characters" class="portal-section">
|
||
<div class="portal-section__head">
|
||
<div>
|
||
<span class="eyebrow">Мои персонажи</span>
|
||
<h2 class="h-serif">Герои аккаунта</h2>
|
||
</div>
|
||
<span class="portal-section__count">{{ characters.length }} {{ characters.length === 1 ? 'герой' : 'героев' }}</span>
|
||
</div>
|
||
|
||
<div v-if="characters.length" class="portal-character-grid">
|
||
<article v-for="character in characters" :key="character.name" class="portal-card portal-character-card">
|
||
<div class="portal-character-card__head">
|
||
<span class="portal-character-level"><span>{{ character.level }}</span></span>
|
||
<span :class="['portal-online', { online: character.online }]">
|
||
{{ character.online ? 'Онлайн' : 'Оффлайн' }}
|
||
</span>
|
||
</div>
|
||
<h3 class="h-serif">{{ character.name }}</h3>
|
||
<p>{{ character.race }} · {{ character.class }}</p>
|
||
<dl>
|
||
<div><dt>Пол</dt><dd>{{ character.gender }}</dd></div>
|
||
<div><dt>Золото</dt><dd>{{ character.money }}</dd></div>
|
||
<div><dt>Время в игре</dt><dd>{{ character.played }}</dd></div>
|
||
</dl>
|
||
</article>
|
||
</div>
|
||
<article v-else class="portal-card portal-empty">
|
||
<span class="portal-empty__moon">☾</span>
|
||
<h3 class="h-serif">Персонажей пока нет</h3>
|
||
<p>Создай первого героя в игре — он появится здесь автоматически.</p>
|
||
<a class="btn btn-primary" :href="urls.client">Скачать лаунчер</a>
|
||
</article>
|
||
</section>
|
||
|
||
<section id="wallet" class="portal-section">
|
||
<div class="portal-section__head">
|
||
<div>
|
||
<span class="eyebrow">Слёзы Элуны</span>
|
||
<h2 class="h-serif">Баланс и операции</h2>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="portal-grid portal-grid--wallet">
|
||
<article class="portal-card portal-wallet-card corners">
|
||
<span class="portal-card-label">Доступно сейчас</span>
|
||
<div class="portal-balance">{{ wallet.formatted_amount }}</div>
|
||
<p>Слёз Элуны</p>
|
||
|
||
<form v-if="wallet.deposit_enabled" class="portal-deposit" method="POST" :action="urls.deposit">
|
||
<input type="hidden" name="_token" :value="csrfToken" />
|
||
<label class="portal-field">
|
||
<span>Сумма пополнения, ₽</span>
|
||
<input
|
||
type="number"
|
||
name="amount"
|
||
:min="wallet.minimum_deposit"
|
||
:max="wallet.maximum_deposit"
|
||
step="0.01"
|
||
:value="wallet.deposit_amount"
|
||
required
|
||
/>
|
||
<small v-if="sharedErrors.amount" class="portal-field-error">{{ sharedErrors.amount }}</small>
|
||
</label>
|
||
<button class="btn btn-primary" type="submit">
|
||
Пополнить <span v-html="arrowSvg"></span>
|
||
</button>
|
||
</form>
|
||
<div v-if="wallet.deposit_enabled" class="portal-rate">
|
||
<span>1 ₽ = {{ wallet.tears_per_ruble }} Слёз Элуны</span>
|
||
<span>От {{ wallet.minimum_deposit }} до {{ wallet.formatted_maximum_deposit }} ₽</span>
|
||
</div>
|
||
</article>
|
||
|
||
<article class="portal-card portal-history-card">
|
||
<span class="portal-card-label">Последние операции</span>
|
||
<div v-if="wallet.transactions.length" class="portal-history">
|
||
<div v-for="transaction in wallet.transactions" :key="transaction.id" class="portal-history__row">
|
||
<span>
|
||
<strong>{{ transaction.label }}</strong>
|
||
<small>{{ transaction.created_at }}</small>
|
||
</span>
|
||
<strong :class="{ positive: transaction.is_positive }">
|
||
{{ transaction.is_positive ? '+' : '' }}{{ transaction.formatted_amount }}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
<div v-else class="portal-history-empty">Операций пока нет.</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="security" class="portal-section">
|
||
<div class="portal-section__head">
|
||
<div>
|
||
<span class="eyebrow">Безопасность</span>
|
||
<h2 class="h-serif">Сменить пароль</h2>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="portal-card portal-security-card corners">
|
||
<div class="portal-security-card__copy">
|
||
<h3 class="h-serif">Защити свой аккаунт</h3>
|
||
<p>После обновления новый пароль сразу начнёт действовать и в игре, и в личном кабинете.</p>
|
||
<ul>
|
||
<li>От 8 до 32 символов</li>
|
||
<li>Хотя бы одна буква и одна цифра</li>
|
||
<li>Не используй пароль от почты</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<form class="portal-password-form" @submit.prevent="updatePassword">
|
||
<label :class="['portal-field', { 'has-error': passwordForm.errors.current_password }]">
|
||
<span>Текущий пароль</span>
|
||
<input v-model="passwordForm.current_password" type="password" autocomplete="current-password" maxlength="32" required />
|
||
<small v-if="passwordForm.errors.current_password">{{ passwordForm.errors.current_password }}</small>
|
||
</label>
|
||
<label :class="['portal-field', { 'has-error': passwordForm.errors.password }]">
|
||
<span>Новый пароль</span>
|
||
<input v-model="passwordForm.password" type="password" autocomplete="new-password" maxlength="32" required />
|
||
<small v-if="passwordForm.errors.password">{{ passwordForm.errors.password }}</small>
|
||
</label>
|
||
<label :class="['portal-field', { 'has-error': passwordForm.errors.password }]">
|
||
<span>Повтори новый пароль</span>
|
||
<input v-model="passwordForm.password_confirmation" type="password" autocomplete="new-password" maxlength="32" required />
|
||
</label>
|
||
<button class="btn btn-primary portal-submit" type="submit" :disabled="passwordForm.processing">
|
||
{{ passwordForm.processing ? 'Обновляем…' : 'Обновить пароль' }}
|
||
<span v-html="arrowSvg"></span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</PortalLayout>
|
||
</template>
|