Files
EpicNext-Cms/src/features/housekeeping/domains/content/pages/poll-results.tsx
T

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>
);
}