173 lines
11 KiB
Vue
173 lines
11 KiB
Vue
<script setup>
|
||
import { computed, nextTick, onMounted, reactive, ref } from 'vue';
|
||
import { arrowSvg, iconPaths, moonMark } from './symbols';
|
||
|
||
const props = defineProps({
|
||
registerEndpoint: { type: String, required: true },
|
||
csrfToken: { type: String, required: true },
|
||
auth: { type: Object, required: true },
|
||
flash: { type: Object, required: true },
|
||
});
|
||
|
||
const hints = {
|
||
username: 'Используй латиницу и цифры. Регистр символов при входе не имеет значения.',
|
||
invite_code: 'Регистрация доступна только по действующему инвайт-коду.',
|
||
};
|
||
|
||
const initialErrors = props.flash.errors || {};
|
||
const submitted = ref(Boolean(props.flash.success));
|
||
const submitting = ref(false);
|
||
const form = reactive({
|
||
username: props.flash.old?.username || '',
|
||
email: props.flash.old?.email || '',
|
||
invite_code: props.flash.old?.invite_code || '',
|
||
password: '',
|
||
password_confirmation: '',
|
||
terms: false,
|
||
});
|
||
const touched = reactive(Object.fromEntries(Object.keys(initialErrors).map((key) => [key, true])));
|
||
const serverErrors = reactive({ ...initialErrors });
|
||
|
||
const clientErrors = computed(() => {
|
||
const errors = {};
|
||
if (form.username && !/^[a-zA-Z0-9]{3,32}$/.test(form.username)) errors.username = 'Только латиница и цифры, 3-32 символа.';
|
||
if (form.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) errors.email = 'Похоже на некорректный email.';
|
||
if (form.invite_code && !/^[A-Za-z0-9-]{6,32}$/.test(form.invite_code)) errors.invite_code = 'Код из букв, цифр и тире, 6-32 символа.';
|
||
if (form.password && form.password.length < 8) errors.password = 'Минимум 8 символов.';
|
||
else if (form.password && !/[A-Za-z]/.test(form.password)) errors.password = 'Должна быть хотя бы одна буква.';
|
||
else if (form.password && !/\d/.test(form.password)) errors.password = 'Должна быть хотя бы одна цифра.';
|
||
if (form.password_confirmation && form.password_confirmation !== form.password) errors.password_confirmation = 'Пароли не совпадают.';
|
||
return errors;
|
||
});
|
||
|
||
const valid = computed(() => Boolean(
|
||
form.username &&
|
||
form.email &&
|
||
form.invite_code &&
|
||
form.password &&
|
||
form.password_confirmation &&
|
||
form.terms &&
|
||
Object.keys(clientErrors.value).length === 0
|
||
));
|
||
|
||
onMounted(() => {
|
||
if (submitted.value || props.flash.error || Object.keys(initialErrors).length) {
|
||
nextTick(() => document.getElementById('join')?.scrollIntoView({ behavior: 'smooth', block: 'start' }));
|
||
}
|
||
});
|
||
|
||
function firstError(field) {
|
||
const value = serverErrors[field];
|
||
return Array.isArray(value) ? value[0] : value;
|
||
}
|
||
|
||
function errorFor(field) {
|
||
return firstError(field) || (touched[field] && clientErrors.value[field]) || null;
|
||
}
|
||
|
||
function updateField(field, value) {
|
||
form[field] = value;
|
||
delete serverErrors[field];
|
||
}
|
||
|
||
function submit(event) {
|
||
if (valid.value) {
|
||
submitting.value = true;
|
||
return;
|
||
}
|
||
|
||
event.preventDefault();
|
||
['username', 'email', 'invite_code', 'password', 'password_confirmation', 'terms'].forEach((field) => {
|
||
touched[field] = true;
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<section id="join" data-screen-label="04 Join">
|
||
<div class="wrap">
|
||
<div v-if="submitted && flash.success" class="join-card join-success reveal is-visible">
|
||
<div class="join-success-mark" v-html="moonMark(56)"></div>
|
||
<span class="eyebrow" style="justify-content: center">Добро пожаловать</span>
|
||
<h3 class="h-display" style="font-size: 36px; margin-top: 16px">Аккаунт создан</h3>
|
||
<p class="lead" style="margin-top: 14px">
|
||
Логин <span style="color: var(--moon); font-style: normal">{{ flash.success_username || form.username }}</span> готов к входу. Открывай лаунчер и жми «Играть».
|
||
</p>
|
||
<div class="hero-cta" style="justify-content: center; margin-top: 28px">
|
||
<a :href="auth.cabinet_url || auth.login_url || '#'" class="btn btn-primary">Войти в личный кабинет <span v-html="arrowSvg"></span></a>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="join-grid reveal">
|
||
<div class="join-intro">
|
||
<span class="eyebrow">Регистрация</span>
|
||
<div style="height: 20px"></div>
|
||
<h2 class="h-display" style="font-size: clamp(36px, 5vw, 56px)">Создай игровой аккаунт</h2>
|
||
<div class="divider-ornament" style="justify-content: flex-start; margin: 22px 0"><span style="opacity: 0.6">✦</span></div>
|
||
<p class="lead" style="font-style: italic">Укажи инвайт-код, логин, почту и пароль - аккаунт будет готов к входу в игру сразу после регистрации.</p>
|
||
<div class="join-note">
|
||
<span class="join-note-k">Закрытый бета-доступ</span>
|
||
<span class="join-note-v">Пока идет закрытый этап: регистрация только по инвайт-кодам. Код можно получить у друзей-игроков или в нашем Discord.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<form class="join-card" method="POST" :action="registerEndpoint" novalidate @submit="submit">
|
||
<input type="hidden" name="_token" :value="csrfToken" />
|
||
<div class="join-card-corner tl" aria-hidden="true"></div>
|
||
<div class="join-card-corner tr" aria-hidden="true"></div>
|
||
<div class="join-card-corner bl" aria-hidden="true"></div>
|
||
<div class="join-card-corner br" aria-hidden="true"></div>
|
||
<div v-if="flash.error" class="join-error" style="margin-bottom: 14px">{{ flash.error }}</div>
|
||
|
||
<div :class="['join-field', { 'has-error': errorFor('username') }]">
|
||
<label class="join-label">Логин</label>
|
||
<div class="join-input"><input type="text" name="username" autocomplete="username" placeholder="Moonwalker" :value="form.username" maxlength="32" @input="updateField('username', $event.target.value)" @blur="touched.username = true" /></div>
|
||
<div v-if="errorFor('username')" class="join-error">{{ errorFor('username') }}</div>
|
||
<div v-else class="join-hint">{{ hints.username }}</div>
|
||
</div>
|
||
|
||
<div :class="['join-field', { 'has-error': errorFor('email') }]">
|
||
<label class="join-label">Email</label>
|
||
<div class="join-input"><input type="email" name="email" autocomplete="email" placeholder="player@example.com" :value="form.email" @input="updateField('email', $event.target.value)" @blur="touched.email = true" /></div>
|
||
<div v-if="errorFor('email')" class="join-error">{{ errorFor('email') }}</div>
|
||
</div>
|
||
|
||
<div :class="['join-field', { 'has-error': errorFor('invite_code') }]">
|
||
<label class="join-label">Инвайт-код</label>
|
||
<div class="join-input">
|
||
<span class="join-input-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" v-html="iconPaths.mask"></svg></span>
|
||
<input type="text" name="invite_code" placeholder="ABCD-EFGH-IJKL" :value="form.invite_code" maxlength="32" style="letter-spacing: 0.14em; font-family: var(--f-mono)" @input="updateField('invite_code', $event.target.value.toUpperCase())" @blur="touched.invite_code = true" />
|
||
</div>
|
||
<div v-if="errorFor('invite_code')" class="join-error">{{ errorFor('invite_code') }}</div>
|
||
<div v-else class="join-hint">{{ hints.invite_code }}</div>
|
||
</div>
|
||
|
||
<div class="join-row">
|
||
<div :class="['join-field', { 'has-error': errorFor('password') }]">
|
||
<label class="join-label">Пароль</label>
|
||
<div class="join-input"><input type="password" name="password" autocomplete="new-password" placeholder="Минимум 8 символов" :value="form.password" maxlength="32" @input="updateField('password', $event.target.value)" @blur="touched.password = true" /></div>
|
||
<div v-if="errorFor('password')" class="join-error">{{ errorFor('password') }}</div>
|
||
</div>
|
||
<div :class="['join-field', { 'has-error': errorFor('password_confirmation') }]">
|
||
<label class="join-label">Повтор пароля</label>
|
||
<div class="join-input"><input type="password" name="password_confirmation" autocomplete="new-password" placeholder="Повтори пароль" :value="form.password_confirmation" maxlength="32" @input="updateField('password_confirmation', $event.target.value)" @blur="touched.password_confirmation = true" /></div>
|
||
<div v-if="errorFor('password_confirmation')" class="join-error">{{ errorFor('password_confirmation') }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
<label class="join-check">
|
||
<input type="checkbox" name="terms" value="1" :checked="form.terms" @change="updateField('terms', $event.target.checked)" />
|
||
<span class="box" aria-hidden="true"></span>
|
||
<span>Я принимаю <a href="#">правила сервера</a> и согласен на обработку данных для создания игрового аккаунта.</span>
|
||
</label>
|
||
<div v-if="errorFor('terms')" class="join-error">{{ errorFor('terms') }}</div>
|
||
|
||
<button type="submit" :class="['btn btn-primary join-submit', { 'is-disabled': !valid || submitting }]" :disabled="!valid || submitting">
|
||
{{ submitting ? 'Создаем аккаунт...' : 'Зарегистрировать аккаунт' }} <span v-html="arrowSvg"></span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|