Школьники не должны придумывать пароль, чтобы пройти тест. Бэкенд уже
поддерживал это: 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>
76 lines
2.8 KiB
TypeScript
76 lines
2.8 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { useNavigate } from "react-router-dom";
|
||
import * as api from "../api/client";
|
||
import { Button } from "../components/ui";
|
||
import { SearchableDropdown, type Option } from "../components/SearchableDropdown";
|
||
import { useAuth } from "../auth/AuthContext";
|
||
|
||
// Вход без регистрации: школьник выбирает только школу и класс, аккаунт
|
||
// и пароль не нужны. Результат теста запишется в базу со снимком
|
||
// организации/группы, но без привязки к пользователю.
|
||
export function AnonymousEntryPage() {
|
||
const navigate = useNavigate();
|
||
const { loginAnonymous } = useAuth();
|
||
|
||
const [orgId, setOrgId] = useState<string | null>(null);
|
||
const [groupId, setGroupId] = useState<string | null>(null);
|
||
const [orgs, setOrgs] = useState<api.Organization[]>([]);
|
||
const [groups, setGroups] = useState<api.Group[]>([]);
|
||
const [error, setError] = useState("");
|
||
|
||
useEffect(() => {
|
||
let cancelled = false;
|
||
(async () => {
|
||
const [o, g] = await Promise.all([api.getOrganizations(), api.getGroups()]);
|
||
if (cancelled) return;
|
||
setOrgs(o);
|
||
setGroups(g);
|
||
})();
|
||
return () => {
|
||
cancelled = true;
|
||
};
|
||
}, []);
|
||
|
||
const onOrgChange = (id: string | null) => {
|
||
setOrgId(id);
|
||
const org = orgs.find((o) => o.id === id);
|
||
if (org?.group_id) setGroupId(org.group_id);
|
||
};
|
||
|
||
const orgOptions: Option[] = orgs.map((o) => ({ key: o.id, text: o.name_organization }));
|
||
const groupOptions: Option[] = groups.map((g) => ({ key: g.id, text: g.name_group }));
|
||
|
||
const proceed = () => {
|
||
if (!orgId || !groupId) {
|
||
setError("Выберите школу и класс");
|
||
return;
|
||
}
|
||
loginAnonymous(orgId, groupId);
|
||
navigate("/");
|
||
};
|
||
|
||
return (
|
||
<div className="auth-wrap">
|
||
<div className="auth-logo">ЦОПП</div>
|
||
<div className="auth-sub">Пройти тест без регистрации — выберите школу и класс</div>
|
||
<form
|
||
className="auth-card"
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
proceed();
|
||
}}
|
||
>
|
||
<SearchableDropdown label="Школа" hint="Выберите школу..." options={orgOptions} value={orgId} onChange={onOrgChange} />
|
||
<SearchableDropdown label="Класс" hint="Выберите класс..." options={groupOptions} value={groupId} onChange={setGroupId} />
|
||
{error && <div className="error-text">{error}</div>}
|
||
<div className="wrap-row">
|
||
<Button type="button" variant="ghost" onClick={() => navigate("/login")}>
|
||
Назад
|
||
</Button>
|
||
<Button type="submit">Продолжить</Button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
);
|
||
}
|