Initial commit MKD fixes
This commit is contained in:
173
components/LoginPage.tsx
Executable file
173
components/LoginPage.tsx
Executable file
@@ -0,0 +1,173 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user