Школьники не должны придумывать пароль, чтобы пройти тест. Бэкенд уже
поддерживал это: POST /polls/{poll_id}/responses принимает user_id как
Optional, а Response.group_id/organization_id — самостоятельные nullable
снимки школы/класса, не завязанные на аккаунт. Не хватало только
фронтенда — все роуты были жёстко завёрнуты в ProtectedRoute с
обязательным JWT.
- AuthContext: параллельная "анонимная идентичность" (org+group в
localStorage, без токена и без запроса к API)
- ProtectedRoute пускает по токену ИЛИ анонимной сессии; новый
AccountRoute — строго по токену, для /my-tests, /profile,
/response/:id (у анонимной сессии нет истории — её негде хранить)
- Новая страница /anonymous — выбор школы+класса тем же
SearchableDropdown, что и в регистрации
- PollPage при отправке ответа берёт org/group из анонимной сессии,
если пользователь не залогинен
- HomePage скрывает "Мои тесты" и профиль для анонимов, добавлена
кнопка выхода из анонимного режима
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import { useState } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { useAuth } from "../auth/AuthContext";
|
||
import { Button, TextField } from "../components/ui";
|
||
import { AgreementDialog } from "../components/AgreementDialog";
|
||
import { IconPerson, IconLock } from "../components/icons";
|
||
|
||
export function LoginPage() {
|
||
const { login } = useAuth();
|
||
const navigate = useNavigate();
|
||
|
||
const [username, setUsername] = useState("");
|
||
const [password, setPassword] = useState("");
|
||
const [error, setError] = useState("");
|
||
const [busy, setBusy] = useState(false);
|
||
const [askReg, setAskReg] = useState(false);
|
||
|
||
const doLogin = async () => {
|
||
if (!username.trim() || !password) {
|
||
setError("Заполните логин и пароль");
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
setError("");
|
||
const err = await login(username.trim(), password);
|
||
setBusy(false);
|
||
if (err) {
|
||
setError(err);
|
||
} else {
|
||
navigate("/");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="auth-wrap">
|
||
<div className="auth-logo">ЦОПП</div>
|
||
<div className="auth-sub">
|
||
Это проект ЦОПП по проведению исследовательских тестирований
|
||
</div>
|
||
<form
|
||
className="auth-card"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
doLogin();
|
||
}}
|
||
>
|
||
<TextField
|
||
label="Логин"
|
||
placeholder="Введите логин..."
|
||
icon={<IconPerson />}
|
||
value={username}
|
||
onChange={(e) => setUsername(e.target.value)}
|
||
/>
|
||
<TextField
|
||
label="Пароль"
|
||
placeholder="Введите пароль..."
|
||
icon={<IconLock />}
|
||
password
|
||
value={password}
|
||
onChange={(e) => setPassword(e.target.value)}
|
||
/>
|
||
{error && <div className="error-text">{error}</div>}
|
||
<Button type="submit" disabled={busy} block>
|
||
{busy ? "Вход..." : "Войти"}
|
||
</Button>
|
||
<button type="button" className="link-btn" onClick={() => setAskReg(true)}>
|
||
Нет аккаунта? Зарегистрироваться
|
||
</button>
|
||
<button type="button" className="link-btn" onClick={() => navigate("/anonymous")}>
|
||
Пройти тест без регистрации
|
||
</button>
|
||
</form>
|
||
|
||
{askReg && (
|
||
<AgreementDialog
|
||
onClose={() => setAskReg(false)}
|
||
onAccept={() => navigate("/register")}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|