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>
This commit is contained in:
2026-03-17 19:35:54 +03:00
parent 1fd12a6dbb
commit e59bc752b0
3 changed files with 37 additions and 8 deletions

View File

@@ -330,6 +330,7 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
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)
@@ -380,8 +381,9 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
type="text"
className={cn('input', errors.hotelName && 'border-red-400 focus:ring-red-400')}
placeholder="Grand Palace Hotel"
autoCapitalize="words"
value={hotelName}
onChange={e => { setHotelName(e.target.value); setErrors(prev => ({ ...prev, hotelName: '' })) }}
onChange={e => { setHotelName(e.target.value.replace(/(?:^|\s)\S/g, c => c.toUpperCase())); setErrors(prev => ({ ...prev, hotelName: '' })) }}
/>
</Field>
@@ -399,8 +401,9 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
type="text"
className={cn('input', errors.contact && 'border-red-400 focus:ring-red-400')}
placeholder="Иванов Иван Иванович"
autoCapitalize="words"
value={contact}
onChange={e => { setContact(e.target.value); setErrors(prev => ({ ...prev, contact: '' })) }}
onChange={e => { setContact(e.target.value.replace(/(?:^|\s)\S/g, c => c.toUpperCase())); setErrors(prev => ({ ...prev, contact: '' })) }}
/>
</Field>
@@ -438,9 +441,17 @@ function RegisterForm({ onSwitch }: { onSwitch: () => void }) {
</button>
</div>
{password && (
<div className="mt-1.5 flex gap-1">
{[password.length >= 8, /[A-Z]/.test(password), /[0-9]/.test(password)].map((ok, i) => (
<div key={i} className={cn('h-1 flex-1 rounded-full transition-colors', ok ? 'bg-emerald-500' : 'bg-slate-200 dark:bg-slate-600')} />
<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>
)}

View File

@@ -21,6 +21,7 @@ export function ResetPasswordPage() {
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)
@@ -100,9 +101,17 @@ export function ResetPasswordPage() {
</button>
</div>
{password && (
<div className="mt-1.5 flex gap-1">
{[password.length >= 8, /[A-Z]/.test(password), /[0-9]/.test(password)].map((ok, i) => (
<div key={i} className={cn('h-1 flex-1 rounded-full transition-colors', ok ? 'bg-emerald-500' : 'bg-slate-200 dark:bg-slate-600')} />
<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>
)}