Files
api-copp/web-react/src/pages/ResponsePage.tsx
jze9 eac61923f4 feat(web): React-фронтенд вместо Flet + fix restore rmtree на bind-mount
web-react/: SPA Vite+React+TS, nginx раздаёт статику и проксирует /api на api:8000 по локалке. docker-compose: web(Flet)->web-react на :80. transfer_crud.backup_restore: чистим содержимое data/radar вместо rmtree точки монтирования (EBUSY).
2026-06-02 13:56:44 +00:00

103 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import * as api from "../api/client";
import { Layout } from "../components/Layout";
import { Card, CenterSpinner } from "../components/ui";
import { RadarBlock } from "../components/RadarBlock";
import { IconCheckCircle, IconRadioUnchecked } from "../components/icons";
interface State {
response: api.ResponseDetail;
poll: api.Poll;
hasRadar: boolean;
}
export function ResponsePage() {
const { responseId = "" } = useParams();
const [state, setState] = useState<State | null>(null);
const [error, setError] = useState("");
useEffect(() => {
let cancelled = false;
(async () => {
const response = await api.getResponse(responseId);
if (cancelled) return;
if (!response) {
setError("Результат не найден");
return;
}
const poll = await api.getPoll(response.poll_id);
if (cancelled) return;
if (!poll) {
setError("Тест не найден");
return;
}
const radar = await api.getRadarResult(responseId);
if (cancelled) return;
setState({ response, poll, hasRadar: !!radar?.image_url });
})();
return () => {
cancelled = true;
};
}, [responseId]);
return (
<Layout title="Результаты теста" back="/my-tests" showProfile={false}>
{error ? (
<p className="error-text">{error}</p>
) : !state ? (
<CenterSpinner />
) : (
<ResultBody {...state} responseId={responseId} />
)}
</Layout>
);
}
function ResultBody({ response, poll, hasRadar, responseId }: State & { responseId: string }) {
const questions = new Map(poll.questions?.map((q) => [q.id, q]) ?? []);
const submittedAt = response.submitted_at.slice(0, 19).replace("T", " ");
return (
<div className="stack">
<div className="col-gap" style={{ gap: "0.25rem" }}>
<span className="title-lg">{poll.title}</span>
<span className="text-sm muted">Пройден: {submittedAt}</span>
</div>
{hasRadar && (
<Card alt>
<div className="center-col">
<span className="title-md">Результат</span>
<RadarBlock responseId={responseId} />
</div>
</Card>
)}
<span className="title-md">Ответы</span>
{response.answers.map((ans, i) => {
const q = questions.get(ans.question_id);
if (!q) return null;
return (
<Card key={i}>
<div className="col-gap">
<span className="bold">{q.text}</span>
{q.choices.map((c) => {
const selected = c.id === ans.choice_id;
return (
<div key={c.id} className={"choice-tile" + (selected ? " selected" : "")}>
<span className="check">
{selected ? <IconCheckCircle /> : <IconRadioUnchecked />}
</span>
<span>{c.text}</span>
</div>
);
})}
</div>
</Card>
);
})}
</div>
);
}