Files
EpicNext-Cms/src/app/(site)/polls/[id]/poll-vote-form.tsx
T
openhands 17847545dd
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m52s
Improvements: remove dead config, fix ESM, add URL validation, unify types, add missing logging
- Remove .prettierrc (dead config, Biome replaces Prettier)
- Rename lighthouserc.json to lighthouserc.cjs with module.exports for ESM compat
- Add logger.warn to empty catch blocks in auth, register, site-settings, prisma-cache, redis, security, rate-limit
- Unify ActionResult type: action-helper.ts uses 'ok' consistent with safe-action-shared.ts
- Add noUnusedLocals + noUnusedParameters to tsconfig + fix 25 pre-existing unused vars
- Replace barrel export src/types/index.ts with direct @/types/common imports
- Make trustHost conditional (development only) in auth.ts
- Add pre-flight URL validation to update-Nitrov3.sh to catch image.library.url misconfigurations
- Improve NITRO_IMAGE_LIBRARY_URL content validation in pre-flight & post-compute checks
2026-07-26 20:28:11 +02:00

167 lines
3.9 KiB
TypeScript

"use client";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { toast } from "sonner";
import { voteOnPoll } from "@/actions/polls";
import { useServerAction } from "@/hooks/use-server-action";
interface Question {
id: number;
question: string;
type: string;
options: string;
}
function parseOptions(options: string): string[] {
return options
.split("\n")
.map((o) => o.trim())
.filter(Boolean);
}
export function PollVoteForm({
pollId,
questions,
}: {
pollId: number;
questions: Question[];
}) {
const t = useTranslations("pages.polls");
const { run, isPending } = useServerAction();
const [answers, setAnswers] = useState<Record<number, string>>({});
const [multi, setMulti] = useState<Record<number, string[]>>({});
function setSingle(questionId: number, value: string) {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
}
function toggleMulti(questionId: number, option: string) {
setMulti((prev) => {
const current = prev[questionId] ?? [];
const next = current.includes(option)
? current.filter((o) => o !== option)
: [...current, option];
return { ...prev, [questionId]: next };
});
}
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const votes = questions.map((q) => {
if (q.type === "multiple") {
return {
questionId: q.id,
answer: (multi[q.id] ?? []).join("\n"),
};
}
return {
questionId: q.id,
answer: (answers[q.id] ?? "").trim(),
};
});
if (votes.some((v) => !v.answer)) {
toast.error(t("voteError"));
return;
}
run(() => voteOnPoll({ pollId, votes }), {
successMessage: t("voteSuccess"),
errorMessage: t("voteError"),
});
}
return (
<form onSubmit={handleSubmit} style={{ display: "grid", gap: "1.25rem" }}>
{questions.map((q) => {
const options = parseOptions(q.options);
return (
<fieldset
key={q.id}
style={{
margin: 0,
padding: "0.85rem 1rem",
border:
"1px solid color-mix(in srgb, var(--color-text-muted) 18%, transparent)",
borderRadius: "0.65rem",
}}
>
<legend style={{ padding: "0 0.35rem", fontWeight: 700 }}>
{q.question}
</legend>
{q.type === "text" ? (
<textarea
required
maxLength={500}
rows={3}
value={answers[q.id] ?? ""}
onChange={(e) => setSingle(q.id, e.target.value)}
placeholder={t("textPlaceholder")}
style={{
width: "100%",
marginTop: "0.35rem",
resize: "vertical",
}}
/>
) : q.type === "multiple" ? (
<div
style={{ display: "grid", gap: "0.4rem", marginTop: "0.35rem" }}
>
{options.map((opt) => (
<label
key={opt}
style={{
display: "flex",
gap: "0.5rem",
alignItems: "center",
cursor: "pointer",
}}
>
<input
type="checkbox"
checked={(multi[q.id] ?? []).includes(opt)}
onChange={() => toggleMulti(q.id, opt)}
/>
<span>{opt}</span>
</label>
))}
</div>
) : (
<div
style={{ display: "grid", gap: "0.4rem", marginTop: "0.35rem" }}
>
{options.map((opt) => (
<label
key={opt}
style={{
display: "flex",
gap: "0.5rem",
alignItems: "center",
cursor: "pointer",
}}
>
<input
type="radio"
name={`q-${q.id}`}
required
checked={(answers[q.id] ?? "") === opt}
onChange={() => setSingle(q.id, opt)}
/>
<span>{opt}</span>
</label>
))}
</div>
)}
</fieldset>
);
})}
<button type="submit" className="btn btn-primary" disabled={isPending}>
{isPending ? t("voting") : t("submitVote")}
</button>
</form>
);
}