astro_avtourist/frontend/src/pages/auth/sign-in.astro
2026-05-05 23:45:42 +05:00

642 lines
18 KiB
Text
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.

---
import Layout from '@layouts/Layout.astro';
import { SITE_URL } from '@constants';
---
<Layout
title="Вход в аккаунт"
description="Войдите в свой аккаунт для доступа к личному кабинету"
canonicalLink={`${SITE_URL}/auth/sign-in`}
>
<div class="auth-page">
<div class="auth-container">
<div class="auth-card">
<div class="auth-header">
<h1>Вход в аккаунт</h1>
<p>Введите свои данные для входа</p>
</div>
<form class="auth-form" id="sign-in-form">
<div class="form-group honeypot-field">
<input
type="text"
name="website_url"
id="website_url"
tabindex="-1"
autocomplete="off"
class="honeypot"
/>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
placeholder="example@mail.ru"
required
autocomplete="email"
inputmode="email"
/>
<span class="error-message" id="email-error"></span>
</div>
<div class="form-group">
<label for="password">
Пароль
<span class="hint">От 8 до 12 символов</span>
</label>
<div class="password-wrapper">
<input
type="password"
id="password"
name="password"
placeholder="••••••••"
required
autocomplete="current-password"
minlength="8"
maxlength="12"
/>
<button type="button" class="toggle-password" data-target="password">
<svg class="eye-open" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<svg class="eye-closed" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
<line x1="1" y1="1" x2="23" y2="23"></line>
</svg>
</button>
</div>
<span class="error-message" id="password-error"></span>
</div>
<div class="form-options">
<label class="checkbox-label">
<input type="checkbox" name="remember" />
<span>Запомнить меня</span>
</label>
<a href="/auth/forgot-password" class="forgot-link">Забыли пароль?</a>
</div>
<button type="submit" class="btn-submit">
Войти
</button>
</form>
<div class="auth-footer">
<p>Нет аккаунта? <a href="/auth/sign-up">Зарегистрироваться</a></p>
</div>
</div>
</div>
</div>
</Layout>
<style>
/* Основная страница */
.auth-page {
min-height: calc(100vh - 160px);
background: linear-gradient(135deg, #f5f7fa 0%, #e8ecf1 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 6rem 2rem 2rem;
}
.auth-container {
width: 100%;
max-width: 440px;
}
/* Карточка авторизации */
.auth-card {
background: #ffffff;
border-radius: 16px;
padding: 1rem 1.25rem;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
/* Заголовок */
.auth-header {
text-align: center;
margin-bottom: 1rem;
}
.auth-header h1 {
color: #1e3050;
font-size: 1.5rem;
font-weight: 800;
margin: 0 0 0.5rem 0;
letter-spacing: -0.02em;
}
.auth-header p {
color: #64748b;
font-size: 0.9rem;
margin: 0;
}
/* Форма */
.auth-form {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.form-group label {
color: #1e3050;
font-size: 0.8rem;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-group label .hint {
color: #94a3b8;
font-size: 0.75rem;
font-weight: 400;
}
.form-group input {
padding: 0.5rem 0.6rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 0.9rem;
transition: all 0.2s ease;
background: #f8fafc;
}
.form-group input:focus {
outline: none;
border-color: #d4af37;
box-shadow: 0 0 0 3px rgba(212, 175, 55, 0.1);
background: #ffffff;
}
.password-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-wrapper input {
width: 100%;
padding-right: 2.5rem;
}
.toggle-password {
position: absolute;
right: 0.75rem;
background: none;
border: none;
cursor: pointer;
color: #94a3b8;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease;
}
.toggle-password:hover {
color: #d4af37;
}
.toggle-password .eye-closed {
display: none;
}
.toggle-password.active .eye-open {
display: none;
}
.toggle-password.active .eye-closed {
display: block;
}
.form-group input::placeholder {
color: #94a3b8;
}
/* Honeypot поле */
.honeypot-field {
position: absolute;
left: -9999px;
opacity: 0;
pointer-events: none;
}
/* Сообщения об ошибках */
.error-message {
color: #ef4444;
font-size: 0.75rem;
min-height: 0.75rem;
margin-top: 0.15rem;
}
.form-group input.error {
border-color: #ef4444;
}
.form-group input.error:focus {
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
/* Опции формы */
.form-options {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
color: #64748b;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #d4af37;
cursor: pointer;
}
.forgot-link {
color: #d4af37;
text-decoration: none;
font-weight: 500;
transition: color 0.2s ease;
}
.forgot-link:hover {
color: #b8941f;
}
/* Кнопка отправки */
.btn-submit {
background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%);
color: #ffffff;
border: none;
border-radius: 8px;
padding: 0.75rem;
font-size: 0.95rem;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 0.25rem;
}
.btn-submit:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(206, 159, 64, 0.4);
}
.btn-submit:active {
transform: translateY(0);
}
.btn-submit:disabled {
background: #94a3b8;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-submit:disabled:hover {
transform: none;
box-shadow: none;
}
.btn-submit.loading,
.btn-submit.loading:disabled {
position: relative;
color: transparent !important;
pointer-events: none;
opacity: 1 !important;
background: linear-gradient(135deg, #eac26e 0%, #ce9f40 100%) !important;
}
.btn-submit.loading::after {
content: '';
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin-left: -10px;
margin-top: -10px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Подвал формы */
.auth-footer {
text-align: center;
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px solid #e2e8f0;
}
.auth-footer p {
color: #64748b;
font-size: 0.9rem;
margin: 0;
}
.auth-footer a {
color: #d4af37;
text-decoration: none;
font-weight: 600;
transition: color 0.2s ease;
}
.auth-footer a:hover {
color: #b8941f;
}
/* Адаптивность */
@media (max-width: 480px) {
.auth-page {
padding: 5rem 1rem 1.5rem;
}
.auth-card {
padding: 1.5rem 1rem;
}
.auth-header h1 {
font-size: 1.35rem;
}
.auth-header p {
font-size: 0.85rem;
}
.form-group {
gap: 0.15rem;
}
.form-group label {
font-size: 0.8rem;
}
.form-group input {
padding: 0.5rem 0.65rem;
font-size: 0.85rem;
}
.form-group input::placeholder {
font-size: 0.8rem;
}
.error-message {
font-size: 0.7rem;
}
.form-options {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
font-size: 0.85rem;
}
.checkbox-label {
gap: 0.4rem;
}
.checkbox-label input[type="checkbox"] {
width: 14px;
height: 14px;
}
.forgot-link {
font-size: 0.85rem;
}
.btn-submit {
padding: 0.65rem;
font-size: 0.9rem;
margin-top: 0.2rem;
}
}
@media (max-width: 375px) {
.auth-page {
padding: 4.5rem 0.75rem 1rem;
}
.auth-card {
padding: 1.25rem 0.75rem;
border-radius: 12px;
}
.auth-header h1 {
font-size: 1.25rem;
}
.form-group input {
padding: 0.45rem 0.55rem;
font-size: 0.8rem;
border-radius: 6px;
}
.form-group label {
font-size: 0.75rem;
}
.btn-submit {
padding: 0.55rem;
font-size: 0.85rem;
border-radius: 6px;
}
}
@media (max-width: 320px) {
.auth-page {
padding: 4rem 0.5rem 0.75rem;
}
.auth-card {
padding: 1rem 0.5rem;
}
.auth-header h1 {
font-size: 1.15rem;
}
.form-group input {
padding: 0.4rem 0.5rem;
font-size: 0.75rem;
}
.btn-submit {
padding: 0.5rem;
font-size: 0.8rem;
}
}
</style>
<script>
const emailInput = document.getElementById('email') as HTMLInputElement;
const passwordInput = document.getElementById('password') as HTMLInputElement;
function showError(input: HTMLInputElement, message: string) {
const errorSpan = document.getElementById(`${input.id}-error`);
if (errorSpan) {
errorSpan.textContent = message;
}
input.classList.add('error');
}
function clearError(input: HTMLInputElement) {
const errorSpan = document.getElementById(`${input.id}-error`);
if (errorSpan) {
errorSpan.textContent = '';
}
input.classList.remove('error');
}
function validateEmail(value: string): boolean {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(value);
}
function validatePassword(value: string): boolean {
return value.length >= 8 && value.length <= 12;
}
document.querySelectorAll('.toggle-password').forEach(button => {
button.addEventListener('click', () => {
const targetId = button.getAttribute('data-target');
const input = document.getElementById(targetId) as HTMLInputElement;
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
button.classList.toggle('active', isPassword);
}
});
});
emailInput?.addEventListener('input', () => {
const value = emailInput.value.replace(/[^\w@\.\-]/g, '');
emailInput.value = value;
if (emailInput.value && !validateEmail(emailInput.value)) {
showError(emailInput, 'Введите корректный email');
} else {
clearError(emailInput);
}
});
passwordInput?.addEventListener('input', () => {
if (passwordInput.value.length > 12) {
passwordInput.value = passwordInput.value.slice(0, 12);
}
if (passwordInput.value && !validatePassword(passwordInput.value)) {
showError(passwordInput, 'Пароль должен быть 8-12 символов');
} else {
clearError(passwordInput);
}
});
document.getElementById('sign-in-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
const honeypot = formData.get('website_url') as string;
if (honeypot && honeypot.trim() !== '') {
alert('Доступ запрещён');
return;
}
const email = formData.get('email') as string;
const password = formData.get('password') as string;
let hasErrors = false;
if (!validateEmail(email)) {
showError(emailInput, 'Введите корректный email');
hasErrors = true;
}
if (!password || password.trim() === '') {
showError(passwordInput, 'Введите пароль');
hasErrors = true;
} else if (!validatePassword(password)) {
showError(passwordInput, 'Пароль должен быть от 8 до 12 символов');
hasErrors = true;
}
if (hasErrors) {
return;
}
console.log('Вход:', { email, password });
const submitBtn = document.querySelector('.btn-submit') as HTMLButtonElement;
submitBtn.disabled = true;
submitBtn.classList.add('loading');
try {
const response = await fetch('/api/auth/sign-in', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
password,
redirect: new URLSearchParams(window.location.search).get('redirect')
}),
});
const data = await response.json();
if (response.ok && data.token) {
// Сохраняем токен в localStorage
localStorage.setItem('auth_token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
// Сохраняем токен в куку для API
document.cookie = `pb_auth=${data.token}; path=/; max-age=${7 * 24 * 60 * 60}; SameSite=Lax`;
// Перенаправляем на указанный URL или в личный кабинет
const redirectUrl = data.redirect || '/cabinet';
// Если есть хэш с ID комментариев - добавляем его к URL
const hash = new URLSearchParams(window.location.search).get('hash');
const finalUrl = hash ? `${redirectUrl}#comments` : redirectUrl;
window.location.href = finalUrl;
} else if (data.error?.includes('подтверждён')) {
showError(emailInput, data.error);
} else if (data.error?.includes('пароль')) {
showError(passwordInput, data.error);
} else {
showError(passwordInput, data.error || 'Неверный email или пароль');
}
} catch (err) {
showError(passwordInput, 'Ошибка соединения');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Войти';
}
});
</script>