Files
hotelsync/src/pages/ResetPasswordPage.tsx
HotelSync e59bc752b0 Add password letter requirement + labeled strength UI + auto-capitalize name fields
- Reject pure-numeric passwords (must contain ≥1 letter) in RegisterForm, ResetPasswordPage, and backend register/reset-password handlers
- Replace bare strength bars with labeled 2×2 grid: ≥8 символов, Содержит букву, Заглавная буква, Цифра
- Auto-capitalize first letter of each word in "Название отеля" and "Контактное лицо" fields (autoCapitalize="words" + JS handler)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 19:35:54 +03:00

160 lines
7.9 KiB
TypeScript
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 { useState } from 'react'
import { useSearchParams, useNavigate } from 'react-router-dom'
import { Hotel, Eye, EyeOff, AlertCircle, CheckCircle2, Moon, Sun } from 'lucide-react'
import { useTheme } from '../contexts/ThemeContext'
import { api } from '../lib/api'
import { cn } from '../lib/utils'
export function ResetPasswordPage() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const { theme, toggle } = useTheme()
const token = searchParams.get('token') ?? ''
const [password, setPassword] = useState('')
const [password2, setPassword2] = useState('')
const [showPass, setShowPass] = useState(false)
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [done, setDone] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (password.length < 8) { setError('Минимум 8 символов'); return }
if (!/[a-zA-Zа-яА-ЯёЁ]/.test(password)) { setError('Пароль должен содержать хотя бы одну букву'); return }
if (password !== password2) { setError('Пароли не совпадают'); return }
setError('')
setLoading(true)
try {
await api.auth.resetPassword(token, password)
setDone(true)
} catch (err) {
const msg = err instanceof Error ? err.message : 'Ошибка'
setError(msg)
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gradient-to-br from-brand-50 via-white to-slate-100 dark:from-slate-900 dark:via-slate-900 dark:to-brand-950 flex flex-col">
<div className="flex justify-end p-4">
<button onClick={toggle} className="btn-ghost p-2">
{theme === 'light' ? <Moon size={18} /> : <Sun size={18} />}
</button>
</div>
<div className="flex-1 flex items-center justify-center px-6">
<div className="w-full max-w-sm">
{/* Logo */}
<div className="flex items-center gap-2.5 mb-8 justify-center">
<div className="w-9 h-9 rounded-xl bg-brand-600 flex items-center justify-center">
<Hotel size={18} className="text-white" />
</div>
<span className="text-xl font-bold text-slate-900 dark:text-slate-100">
Hotel<span className="text-brand-600">Sync</span>
</span>
</div>
{!token ? (
<div className="text-center py-6">
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm mb-6">
<AlertCircle size={15} /> Ссылка недействительна. Запросите новое письмо.
</div>
<button onClick={() => navigate('/login')} className="btn-primary w-full justify-center py-2.5">
На страницу входа
</button>
</div>
) : done ? (
<div className="text-center py-6">
<div className="w-16 h-16 rounded-full bg-emerald-100 dark:bg-emerald-900/40 flex items-center justify-center mx-auto mb-5">
<CheckCircle2 size={32} className="text-emerald-600 dark:text-emerald-400" />
</div>
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-2">Пароль изменён!</h2>
<p className="text-slate-500 dark:text-slate-400 mb-8">
Теперь вы можете войти с новым паролем.
</p>
<button onClick={() => navigate('/login')} className="btn-primary w-full justify-center py-2.5">
Войти
</button>
</div>
) : (
<>
<h2 className="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-1">Новый пароль</h2>
<p className="text-slate-500 dark:text-slate-400 mb-8">Введите новый пароль для вашего аккаунта</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Новый пароль</label>
<div className="relative">
<input
type={showPass ? 'text' : 'password'}
className="input pr-10"
placeholder="Минимум 8 символов"
value={password}
onChange={e => { setPassword(e.target.value); setError('') }}
required
/>
<button type="button" onClick={() => setShowPass(v => !v)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
{showPass ? <EyeOff size={16} /> : <Eye size={16} />}
</button>
</div>
{password && (
<div className="mt-2 grid grid-cols-2 gap-x-3 gap-y-1">
{[
{ 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 }) => (
<div key={label} className="flex items-center gap-1.5 text-xs">
<div className={cn('w-1.5 h-1.5 rounded-full shrink-0 transition-colors', ok ? 'bg-emerald-500' : 'bg-slate-300 dark:bg-slate-600')} />
<span className={cn('transition-colors', ok ? 'text-emerald-700 dark:text-emerald-400' : 'text-slate-400 dark:text-slate-500')}>{label}</span>
</div>
))}
</div>
)}
</div>
<div>
<label className="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1.5">Повторите пароль</label>
<input
type={showPass ? 'text' : 'password'}
className={cn('input', password2 && password !== password2 && 'border-red-400 focus:ring-red-400')}
placeholder="Повторите пароль"
value={password2}
onChange={e => { setPassword2(e.target.value); setError('') }}
required
/>
{password2 && password !== password2 && (
<p className="mt-1 text-xs text-red-600 dark:text-red-400">Пароли не совпадают</p>
)}
</div>
{error && (
<div className="flex items-center gap-2 p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 text-sm">
<AlertCircle size={15} /> {error}
</div>
)}
<button type="submit" disabled={loading} className="btn-primary w-full justify-center py-2.5">
{loading
? <span className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
: 'Сохранить пароль'}
</button>
</form>
<p className="mt-5 text-center text-sm text-slate-500 dark:text-slate-400">
<button onClick={() => navigate('/login')} className="text-brand-600 dark:text-brand-400 font-medium hover:underline">
Вернуться к входу
</button>
</p>
</>
)}
</div>
</div>
</div>
)
}