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 (
<>
Добро пожаловать
Войдите в аккаунт для доступа к панели управления
{/* 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 аккаунта — мы пришлём ссылку для сброса пароля
>
)
}
// ── 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 (
<>
Регистрация отеля
Заполните данные — мы активируем аккаунт в течение дня
Уже есть аккаунт?{' '}
>
)
}
// ── 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 */}
Современная PMS система
для вашего отеля
Управляйте бронированиями, номерным фондом и уборкой в одном месте.
Синхронизация с Booking.com, Airbnb и другими каналами.
{[
{ value: '500+', label: 'Отелей' },
{ value: '1M+', label: 'Бронирований' },
{ value: '99.9%', label: 'Аптайм' },
].map(s => (
))}
{mode === 'register' && (
{[
'Бесплатный период 14 дней без ввода карты',
'Настройка за 15 минут — импорт из любой PMS',
'Поддержка на русском языке 24/7',
].map(b => (
))}
)}
© 2026 HotelSync · SaaS PMS Platform
{/* Right panel */}
{/* Mobile logo */}
{mode === 'login' &&
setMode('register')} onForgot={() => setMode('forgot')} />}
{mode === 'register' && setMode('login')} />}
{mode === 'forgot' && setMode('login')} />}
)
}