Initial commit MKD fixes

This commit is contained in:
Arsen
2026-02-04 00:17:04 +05:00
commit de94ad707b
312 changed files with 138754 additions and 0 deletions

173
components/LoginPage.tsx Executable file
View 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>
);
};