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' const DEMO_EMAILS = [ 'manager@grand-palace.ru', 'cleaner@grand-palace.ru', 'admin@hotelsync.io', ] 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 ч). Пожалуйста, зарегистрируйтесь снова.
)}
{/* Demo hint */}

Демо-аккаунты (пароль: demo):

{DEMO_EMAILS.map(e => ( ))}

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

) } // ── 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 [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 = 'Пароль должен содержать хотя бы одну букву' 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.general && (
{errors.general}
)}

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

) } // ── 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')} />}
) }