Files
moonwell-web/resources/js/Pages/Landing/Components/Join.vue
T

173 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>