Files
mkd/components/LoginPage.tsx
2026-02-04 00:17:04 +05:00

174 lines
7.0 KiB
TypeScript
Executable File
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 React, { useState, useEffect, useRef } from 'react';
import { backendApi } from '../services/apiClient';
interface LoginPageProps {
onLoginSuccess: (user: { id: string; name: string; role: string; avatar?: string | null }, token: string) => void;
apiError: string | null;
loading: boolean;
}
declare global {
interface Window {
turnstile?: {
render: (container: string | HTMLElement, options: { sitekey: string; callback?: (token: string) => void; 'expired-callback'?: () => void }) => string;
reset: (widgetId?: string) => void;
remove: (widgetId?: string) => void;
};
}
}
export const LoginPage: React.FC<LoginPageProps> = ({ onLoginSuccess, apiError, loading: externalLoading }) => {
const [login, setLogin] = useState('');
const [password, setPassword] = useState('');
const [captchaToken, setCaptchaToken] = useState<string | undefined>(undefined);
const [localError, setLocalError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState<string | null>(null);
const captchaContainerRef = useRef<HTMLDivElement>(null);
const turnstileWidgetIdRef = useRef<string | null>(null);
const loading = externalLoading || submitting;
useEffect(() => {
backendApi.getCaptchaSiteKey().then((r) => setTurnstileSiteKey(r.siteKey || null)).catch(() => setTurnstileSiteKey(null));
}, []);
useEffect(() => {
if (!turnstileSiteKey || !captchaContainerRef.current) return;
const renderTurnstile = () => {
if (captchaContainerRef.current && window.turnstile && !turnstileWidgetIdRef.current) {
turnstileWidgetIdRef.current = window.turnstile.render(captchaContainerRef.current, {
sitekey: turnstileSiteKey,
callback: (token) => setCaptchaToken(token),
'expired-callback': () => setCaptchaToken(undefined),
});
}
};
if (window.turnstile) {
renderTurnstile();
} else {
const t = setInterval(() => {
if (window.turnstile) {
clearInterval(t);
renderTurnstile();
}
}, 100);
return () => clearInterval(t);
}
return () => {
if (turnstileWidgetIdRef.current != null && window.turnstile?.remove) {
window.turnstile.remove(turnstileWidgetIdRef.current);
turnstileWidgetIdRef.current = null;
}
};
}, [turnstileSiteKey]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLocalError(null);
if (!login.trim()) {
setLocalError('Введите логин');
return;
}
if (!password) {
setLocalError('Введите пароль');
return;
}
setSubmitting(true);
try {
const { setAuthToken } = await import('../services/apiClient');
const res = await backendApi.login({
login: login.trim(),
password,
captchaToken: captchaToken || undefined,
});
setAuthToken(res.token);
onLoginSuccess(res.user, res.token);
} catch (err: any) {
setLocalError(err?.message || 'Ошибка входа');
if (turnstileSiteKey && window.turnstile?.reset && turnstileWidgetIdRef.current != null) {
window.turnstile.reset(turnstileWidgetIdRef.current);
setCaptchaToken(undefined);
}
} finally {
setSubmitting(false);
}
};
const error = localError || apiError;
return (
<div className="min-h-screen flex items-center justify-center bg-slate-100 px-4" style={{ fontFamily: 'Inter, sans-serif' }}>
<div className="w-full max-w-md">
<div className="bg-white rounded-[2.5rem] shadow-2xl border border-slate-200 p-8 md:p-10 animate-fade-in">
<div className="flex flex-col items-center mb-8">
<div className="w-14 h-14 rounded-2xl bg-primary-500 flex items-center justify-center mb-4 shadow-lg shadow-primary-900/20">
<svg className="w-8 h-8 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<h1 className="text-xl font-bold text-slate-900 text-center leading-tight">
Центр управления<br />домами
</h1>
<p className="text-xs text-slate-500 mt-2">Войдите в свой аккаунт</p>
</div>
<form onSubmit={handleSubmit} className="space-y-5">
{error && (
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
{error}
</div>
)}
<div>
<label htmlFor="login" className="block text-xs font-bold text-slate-700 mb-1.5 uppercase tracking-wider">
Логин
</label>
<input
id="login"
type="text"
value={login}
onChange={(e) => setLogin(e.target.value)}
autoComplete="username"
placeholder="Введите логин"
className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all"
disabled={loading}
/>
</div>
<div>
<label htmlFor="password" className="block text-xs font-bold text-slate-700 mb-1.5 uppercase tracking-wider">
Пароль
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="current-password"
placeholder="Введите пароль"
className="w-full px-4 py-3 bg-white border border-slate-200 rounded-xl text-slate-800 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent transition-all"
disabled={loading}
/>
</div>
{turnstileSiteKey && (
<div ref={captchaContainerRef} className="min-h-[65px] flex items-center justify-center" />
)}
<button
type="submit"
disabled={loading}
className="w-full py-3.5 px-4 bg-primary-600 hover:bg-primary-700 text-white text-sm font-bold rounded-xl shadow-lg shadow-primary-900/20 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? 'Вход...' : 'Войти'}
</button>
</form>
</div>
<p className="text-center text-xs text-slate-400 mt-6">
Используйте учётные данные, выданные администратором
</p>
</div>
</div>
);
};