Initial MVP skeleton with auth, chat persistence, UI and text LLM integration
This commit is contained in:
268
frontend/app/login/page.tsx
Normal file
268
frontend/app/login/page.tsx
Normal file
@@ -0,0 +1,268 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
const API_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_BASE_URL || "http://127.0.0.1:18000";
|
||||
|
||||
export default function LoginPage() {
|
||||
const [login, setLogin] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ login, password }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let message = "Неверный логин или пароль";
|
||||
try {
|
||||
const data = await res.json();
|
||||
if (data?.detail && typeof data.detail === "string") {
|
||||
message = data.detail;
|
||||
}
|
||||
} catch {
|
||||
// ignore malformed/non-json body
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} catch (err) {
|
||||
setError(err?.message || "Ошибка входа");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: "100vh",
|
||||
backgroundColor: "var(--bg-color)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 24,
|
||||
left: 32,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
fontSize: 20,
|
||||
fontWeight: 600,
|
||||
color: "var(--text-main)",
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 2C6.477 2 2 6.477 2 12C2 17.523 6.477 22 12 22C17.523 22 22 17.523 22 12C22 6.477 17.523 2 12 2ZM12 4C16.418 4 20 7.582 20 12C20 16.418 16.418 20 12 20C7.582 20 4 16.418 4 12C4 7.582 7.582 4 12 4Z"
|
||||
fill="var(--primary)"
|
||||
/>
|
||||
<path
|
||||
d="M15 13H9C8.448 13 8 12.552 8 12C8 11.448 8.448 11 9 11H15C15.552 11 16 11.448 16 12C16 12.552 15.552 13 15 13Z"
|
||||
fill="var(--primary)"
|
||||
/>
|
||||
</svg>
|
||||
AI Chat MVP
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
padding: "40px",
|
||||
borderRadius: "16px",
|
||||
boxShadow: "0 4px 20px rgba(0,0,0,0.05)",
|
||||
width: "100%",
|
||||
maxWidth: "420px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "24px",
|
||||
fontWeight: 500,
|
||||
marginBottom: "32px",
|
||||
color: "var(--text-main)",
|
||||
}}
|
||||
>
|
||||
Вход в систему
|
||||
</h1>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "20px",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<label
|
||||
style={{
|
||||
display: "block",
|
||||
fontSize: "14px",
|
||||
marginBottom: "8px",
|
||||
color: "var(--text-main)",
|
||||
}}
|
||||
>
|
||||
Логин
|
||||
</label>
|
||||
<input
|
||||
value={login}
|
||||
onChange={(e) => setLogin(e.target.value)}
|
||||
type="text"
|
||||
placeholder="admin"
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px 16px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid var(--border-color)",
|
||||
fontSize: "15px",
|
||||
outline: "none",
|
||||
transition: "border-color 0.2s",
|
||||
}}
|
||||
onFocus={(e) => (e.target.style.borderColor = "var(--primary)")}
|
||||
onBlur={(e) =>
|
||||
(e.target.style.borderColor = "var(--border-color)")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
<label style={{ fontSize: "14px", color: "var(--text-main)" }}>
|
||||
Пароль
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
fontSize: "13px",
|
||||
color: "var(--text-muted)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
padding: 0,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{showPassword ? "Скрыть" : "Показать"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
type={showPassword ? "text" : "password"}
|
||||
placeholder="••••••••"
|
||||
required
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "12px 16px",
|
||||
borderRadius: "8px",
|
||||
border: "1px solid var(--border-color)",
|
||||
fontSize: "15px",
|
||||
outline: "none",
|
||||
transition: "border-color 0.2s",
|
||||
}}
|
||||
onFocus={(e) => (e.target.style.borderColor = "var(--primary)")}
|
||||
onBlur={(e) =>
|
||||
(e.target.style.borderColor = "var(--border-color)")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div
|
||||
style={{
|
||||
color: "#d32f2f",
|
||||
fontSize: "14px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
style={{
|
||||
padding: "14px",
|
||||
marginTop: "8px",
|
||||
backgroundColor: isSubmitting
|
||||
? "var(--border-color)"
|
||||
: "var(--primary)",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "8px",
|
||||
fontSize: "15px",
|
||||
fontWeight: 500,
|
||||
transition: "background-color 0.2s",
|
||||
cursor: isSubmitting ? "not-allowed" : "pointer",
|
||||
}}
|
||||
onMouseOver={(e) => {
|
||||
if (!isSubmitting) {
|
||||
e.currentTarget.style.backgroundColor = "var(--primary-hover)";
|
||||
}
|
||||
}}
|
||||
onMouseOut={(e) => {
|
||||
if (!isSubmitting) {
|
||||
e.currentTarget.style.backgroundColor = "var(--primary)";
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isSubmitting ? "Вход..." : "Войти"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div
|
||||
style={{
|
||||
marginTop: "24px",
|
||||
fontSize: "14px",
|
||||
color: "var(--text-muted)",
|
||||
}}
|
||||
>
|
||||
Вход доступен только для пользователей, созданных администратором.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user