206 lines
5.4 KiB
TypeScript
206 lines
5.4 KiB
TypeScript
import type { ContentPollDetailPayload } from "../queries/content-queries";
|
|
import type { ContentPollListState } from "./poll-workflow";
|
|
|
|
export interface ContentPollResultsProps {
|
|
readonly pollId: string;
|
|
readonly detail: ContentPollDetailPayload;
|
|
readonly list: Pick<
|
|
ContentPollListState,
|
|
"responseQuestionId" | "responsePageSize" | "responseOffset"
|
|
>;
|
|
}
|
|
|
|
function displayDate(value: string): string {
|
|
return new Intl.DateTimeFormat("en-GB", {
|
|
dateStyle: "medium",
|
|
timeStyle: "short",
|
|
timeZone: "UTC",
|
|
}).format(new Date(value));
|
|
}
|
|
|
|
function responseHref(
|
|
pollId: string,
|
|
questionId: string,
|
|
pageSize: number,
|
|
offset: number,
|
|
): string {
|
|
const params = new URLSearchParams();
|
|
params.set("responseQuestionId", questionId);
|
|
params.set("responsePageSize", String(pageSize));
|
|
params.set("responseOffset", String(Math.max(0, offset)));
|
|
return `/ase-next/content/engagement/polls/${encodeURIComponent(pollId)}?${params.toString()}`;
|
|
}
|
|
|
|
function voteLabel(count: number): string {
|
|
return `${count} ${count === 1 ? "vote" : "votes"}`;
|
|
}
|
|
|
|
export function ContentPollResults({
|
|
pollId,
|
|
detail,
|
|
list,
|
|
}: ContentPollResultsProps) {
|
|
const choiceQuestions = detail.questions.filter(
|
|
(question) => question.type !== "text",
|
|
);
|
|
const textQuestions = detail.questions.filter(
|
|
(question) => question.type === "text",
|
|
);
|
|
const selectedQuestionId = detail.textResponses.questionId;
|
|
const previousOffset = Math.max(
|
|
0,
|
|
list.responseOffset - list.responsePageSize,
|
|
);
|
|
const nextOffset = list.responseOffset + list.responsePageSize;
|
|
|
|
return (
|
|
<section className="space-y-5" aria-labelledby="poll-results-heading">
|
|
<h2 id="poll-results-heading">Results</h2>
|
|
|
|
{choiceQuestions.length === 0 ? (
|
|
<p>No choice results are available.</p>
|
|
) : (
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
{choiceQuestions.map((question) => (
|
|
<section
|
|
key={question.id}
|
|
className="space-y-3 rounded border border-[var(--admin-border)] p-4"
|
|
>
|
|
<h3>{question.question}</h3>
|
|
<p className="text-sm text-[var(--admin-text-muted)]">
|
|
{voteLabel(question.answerCount)} received
|
|
</p>
|
|
<ul className="space-y-2">
|
|
{question.choiceResults.map((result) => {
|
|
const percentage = Math.round(
|
|
(result.count / Math.max(1, question.answerCount)) * 100,
|
|
);
|
|
return (
|
|
<li key={result.option}>
|
|
<div className="flex justify-between gap-3">
|
|
<span>{result.option}</span>
|
|
<span>
|
|
{voteLabel(result.count)} · {percentage}%
|
|
</span>
|
|
</div>
|
|
<progress
|
|
value={percentage}
|
|
max={100}
|
|
aria-label={`${result.option}: ${percentage}%`}
|
|
/>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</section>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<section className="space-y-3" aria-labelledby="text-results-heading">
|
|
<h3 id="text-results-heading">Free-text responses</h3>
|
|
{textQuestions.length === 0 ? (
|
|
<p>No free-text questions are configured.</p>
|
|
) : (
|
|
<>
|
|
<nav aria-label="Free-text questions">
|
|
<ul className="flex flex-wrap gap-2">
|
|
{textQuestions.map((question) => (
|
|
<li key={question.id}>
|
|
<a
|
|
href={responseHref(
|
|
pollId,
|
|
question.id,
|
|
list.responsePageSize,
|
|
0,
|
|
)}
|
|
aria-current={
|
|
question.id === selectedQuestionId ? "page" : undefined
|
|
}
|
|
>
|
|
{question.question}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
|
|
{detail.textResponses.items.length === 0 ? (
|
|
<p>No responses for this question.</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table>
|
|
<caption className="sr-only">
|
|
Free-text Poll responses
|
|
</caption>
|
|
<thead>
|
|
<tr>
|
|
<th scope="col">User</th>
|
|
<th scope="col">Answer</th>
|
|
<th scope="col">Submitted</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{detail.textResponses.items.map((response) => (
|
|
<tr key={response.id}>
|
|
<td>
|
|
{response.username ?? "Unavailable user"} (user{" "}
|
|
{response.userId})
|
|
</td>
|
|
<td className="whitespace-pre-wrap">
|
|
{response.answer}
|
|
</td>
|
|
<td>
|
|
<time dateTime={response.createdAt}>
|
|
{displayDate(response.createdAt)} UTC
|
|
</time>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
|
|
{selectedQuestionId &&
|
|
(list.responseOffset > 0 ||
|
|
nextOffset < detail.textResponses.total) ? (
|
|
<nav
|
|
aria-label="Free-text response pages"
|
|
className="flex justify-between"
|
|
>
|
|
{list.responseOffset > 0 ? (
|
|
<a
|
|
href={responseHref(
|
|
pollId,
|
|
selectedQuestionId,
|
|
list.responsePageSize,
|
|
previousOffset,
|
|
)}
|
|
>
|
|
Previous
|
|
</a>
|
|
) : (
|
|
<span />
|
|
)}
|
|
{nextOffset < detail.textResponses.total ? (
|
|
<a
|
|
href={responseHref(
|
|
pollId,
|
|
selectedQuestionId,
|
|
list.responsePageSize,
|
|
nextOffset,
|
|
)}
|
|
>
|
|
Next
|
|
</a>
|
|
) : null}
|
|
</nav>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</section>
|
|
</section>
|
|
);
|
|
}
|