import { useState } from 'react' import { Navigate, useNavigate, useSearchParams } from 'react-router-dom' import { Hotel, Eye, EyeOff, Moon, Sun, AlertCircle, CheckCircle2, ArrowLeft, } from 'lucide-react' import { useAuth } from '../contexts/AuthContext' import { useTheme } from '../contexts/ThemeContext' import { cn } from '../lib/utils' import { api, ApiError } from '../lib/api' type LegalType = 'ooo' | 'ip' | 'ao' | 'pao' | 'other' const LEGAL_TYPES: { id: LegalType; label: string }[] = [ { id: 'ooo', label: 'ООО' }, { id: 'ip', label: 'ИП' }, { id: 'ao', label: 'АО' }, { id: 'pao', label: 'ПАО' }, { id: 'other', label: 'Другое'}, ] // suppress unused-import lint in case LEGAL_TYPES is used elsewhere void LEGAL_TYPES // ── Field component — defined OUTSIDE RegisterForm to prevent unmount on re-render interface FieldProps { label: string error?: string children: React.ReactNode } function Field({ label, error, children }: FieldProps) { return (
{children} {error && (

{error}

)}
) } // ── Login form ───────────────────────────────────────────────────────────────── function LoginForm({ onSwitch, onForgot }: { onSwitch: () => void; onForgot: () => void }) { const { login } = useAuth() const navigate = useNavigate() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showPass, setShowPass] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [unconfirmedEmail, setUnconfirmedEmail] = useState(null) const [resendLoading, setResendLoading] = useState(false) const [resendDone, setResendDone] = useState(false) const [searchParams] = useSearchParams() const confirmed = searchParams.get('confirmed') === '1' const resetDone = searchParams.get('reset') === 'success' const tokenError = searchParams.get('error') const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() // don't clear error here — clear it onChange; clearing here causes "flash" setLoading(true) try { const loggedIn = await login(email, password) if (!loggedIn) { setError('Неверный email или пароль'); return } navigate('/calendar') } catch (err) { if (err instanceof ApiError && err.status === 403) { setUnconfirmedEmail(email) setResendDone(false) } else { const msg = err instanceof Error ? err.message : 'Ошибка соединения' setError(msg) } } finally { setLoading(false) } } const handleResend = async () => { if (!unconfirmedEmail) return setResendLoading(true) try { await api.auth.resendConfirmation(unconfirmedEmail) setResendDone(true) } catch { /* silent */ } finally { setResendLoading(false) } } return ( <>

Добро пожаловать

Войдите в аккаунт для доступа к панели управления

{ setEmail(e.target.value); setError(''); setUnconfirmedEmail(null) }} required />
{ setPassword(e.target.value); setError(''); setUnconfirmedEmail(null) }} required />
{error && (
{error}
)} {unconfirmedEmail && (
Email {unconfirmedEmail} ещё не подтверждён. Проверьте почту и перейдите по ссылке из письма.
{resendDone ? (
Письмо отправлено повторно
) : ( )}
)} {confirmed && (
Email подтверждён! Теперь вы можете войти.
)} {resetDone && (
Пароль успешно изменён. Войдите с новым паролем.
)} {tokenError === 'invalid_token' && (
Ссылка недействительна или уже использована.
)} {tokenError === 'token_expired' && (
Ссылка истекла (24 ч). Пожалуйста, зарегистрируйтесь снова.
)}

Нет аккаунта?{' '}

) } // ── Forgot password form ──────────────────────────────────────────────────────── function ForgotPasswordForm({ onBack }: { onBack: () => void }) { const [email, setEmail] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [done, setDone] = useState(false) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() setError('') setLoading(true) try { await api.auth.forgotPassword(email) setDone(true) } catch (err) { const msg = err instanceof Error ? err.message : 'Ошибка' setError(msg) } finally { setLoading(false) } } if (done) { return (

Письмо отправлено

Если аккаунт с адресом {email} существует, на него придёт письмо со ссылкой для сброса пароля.

Ссылка действительна 1 час.

) } return ( <>

Восстановление пароля

Введите email аккаунта — мы пришлём ссылку для сброса пароля

{ setEmail(e.target.value); setError('') }} required autoFocus />
{error && (
{error}
)}
) } // ── Register form ────────────────────────────────────────────────────────────── function RegisterForm({ onSwitch }: { onSwitch: () => void }) { const [done, setDone] = useState(false) const [hotelName, setHotelName] = useState('') const [address, setAddress] = useState('') const [contact, setContact] = useState('') const [email, setEmail] = useState('') const [phone, setPhone] = useState('') const [password, setPassword] = useState('') const [showPass, setShowPass] = useState(false) const [agreed, setAgreed] = useState(false) const [showTerms, setShowTerms] = useState(false) const [loading, setLoading] = useState(false) const [errors, setErrors] = useState>({}) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() const err: Record = {} if (!hotelName.trim()) err.hotelName = 'Введите название отеля' if (!contact.trim()) err.contact = 'Введите контактное лицо' if (!email.trim()) err.email = 'Введите email' else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) err.email = 'Некорректный email' if (password.length < 8) err.password = 'Минимум 8 символов' else if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) err.password = 'Пароль должен содержать хотя бы одну букву' if (!agreed) err.agreed = 'Необходимо принять пользовательское соглашение' setErrors(err) if (Object.keys(err).length > 0) return setLoading(true) try { await api.auth.register({ hotelName, address, contact, email, phone, password }) setDone(true) } catch (regErr) { const msg = regErr instanceof Error ? regErr.message : 'Ошибка регистрации' if (msg.includes('уже существует')) { setErrors({ email: 'Пользователь с таким email уже существует' }) } else { setErrors({ general: msg }) } } finally { setLoading(false) } } if (done) { return (

Заявка отправлена!

Мы получили данные отеля {hotelName}.

На адрес {email} отправлено письмо с подтверждением. Перейдите по ссылке в письме, чтобы активировать аккаунт.

) } return ( <>

Регистрация отеля

Заполните данные — мы активируем аккаунт в течение дня

{ setHotelName(e.target.value.replace(/(?:^|\s)\S/g, c => c.toUpperCase())); setErrors(prev => ({ ...prev, hotelName: '' })) }} /> setAddress(e.target.value)} /> { setContact(e.target.value.replace(/(?:^|\s)\S/g, c => c.toUpperCase())); setErrors(prev => ({ ...prev, contact: '' })) }} /> { setEmail(e.target.value); setErrors(prev => ({ ...prev, email: '' })) }} /> setPhone(e.target.value)} />
{ setPassword(e.target.value); setErrors(prev => ({ ...prev, password: '' })) }} />
{password && (
{[ { ok: password.length >= 8, label: '≥ 8 символов' }, { ok: /[a-zA-Zа-яА-ЯёЁ]/.test(password), label: 'Содержит букву' }, { ok: /[A-ZА-ЯЁ]/.test(password), label: 'Заглавная буква' }, { ok: /[0-9]/.test(password), label: 'Цифра' }, ].map(({ ok, label }) => (
{label}
))}
)} {/* Согласие с пользовательским соглашением */}
{errors.agreed && (

{errors.agreed}

)}
{errors.general && (
{errors.general}
)}

Уже есть аккаунт?{' '}

{/* Модальное окно — пользовательское соглашение */} {showTerms && (
setShowTerms(false)} >
e.stopPropagation()} >

Пользовательское соглашение

Редакция от 1 января 2026 г.

Настоящее Пользовательское соглашение регулирует отношения между ООО «ХотелСинк» (далее — Сервис) и пользователем, зарегистрировавшим учётную запись.

1. Предмет соглашения. Сервис предоставляет доступ к облачной PMS-системе HotelSync для управления гостиничными операциями.

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

3. Персональные данные. Обработка персональных данных осуществляется в соответствии с Федеральным законом № 152-ФЗ «О персональных данных».

4. Ограничение ответственности. Сервис не несёт ответственности за прямые или косвенные убытки, возникшие вследствие использования или невозможности использования платформы.

5. Изменение условий. Сервис вправе изменять условия соглашения, уведомив пользователей не менее чем за 7 дней.

По вопросам: support@hotelsync.ru

)} ) } // ── Page ─────────────────────────────────────────────────────────────────────── export function LoginPage() { const { user } = useAuth() const { theme, toggle } = useTheme() const [mode, setMode] = useState<'login' | 'register' | 'forgot'>('login') if (user) { return } return (
{/* Left panel — branding */}
HotelSync

Современная PMS система
для вашего отеля

Управляйте бронированиями, номерным фондом и уборкой в одном месте. Синхронизация с Booking.com, Airbnb и другими каналами.

{[ { value: '500+', label: 'Отелей' }, { value: '1M+', label: 'Бронирований' }, { value: '99.9%', label: 'Аптайм' }, ].map(s => (

{s.value}

{s.label}

))}
{mode === 'register' && (
{[ 'Бесплатный период 14 дней без ввода карты', 'Настройка за 15 минут — импорт из любой PMS', 'Поддержка на русском языке 24/7', ].map(b => (

{b}

))}
)}

© 2026 HotelSync · SaaS PMS Platform

{/* Right panel */}
{/* Mobile logo */}
HotelSync
{mode === 'login' && setMode('register')} onForgot={() => setMode('forgot')} />} {mode === 'register' && setMode('login')} />} {mode === 'forgot' && setMode('login')} />}
) }