Skip to content

Commit

Permalink
Added multiple polls voting support
Browse files Browse the repository at this point in the history
  • Loading branch information
dkildar committed Jun 6, 2024
1 parent dcd4ea3 commit 2c0502c
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 43 deletions.
1 change: 1 addition & 0 deletions src/common/features/polls/api/get-poll-details-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface GetPollDetailsQueryResponse {
poll_voters?: { name: string; choice_num: number }[];
post_body: string;
post_title: string;
max_choices_voted?: number;
preferred_interpretation: string;
protocol_version: number;
question: string;
Expand Down
56 changes: 32 additions & 24 deletions src/common/features/polls/api/sign-poll-vote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,27 @@ export function useSignPollVoteByKey(poll: ReturnType<typeof useGetPollDetailsQu

return useMutation({
mutationKey: ["sign-poll-vote", poll?.author, poll?.permlink],
mutationFn: async ({ choice }: { choice: string }) => {
mutationFn: async ({ choices }: { choices: Set<string> }) => {
if (!poll || !activeUser) {
error(_t("polls.not-found"));
return;
}

const choiceNum = poll.poll_choices?.find((pc) => pc.choice_text === choice)?.choice_num;
if (typeof choiceNum !== "number") {
const choiceNums = poll.poll_choices
?.filter((pc) => choices.has(pc.choice_text))
?.map((i) => i.choice_num);
if (choiceNums.length === 0) {
error(_t("polls.not-found"));
return;
}

await broadcastPostingJSON(activeUser.username, "polls", {
poll: poll.poll_trx_id,
action: "vote",
choice: choiceNum
choices: choiceNums
});

return { choiceNum };
return { choiceNums: choiceNums };
},
onSuccess: (resp) =>
queryClient.setQueryData<ReturnType<typeof useGetPollDetailsQuery>["data"]>(
Expand All @@ -40,14 +42,18 @@ export function useSignPollVoteByKey(poll: ReturnType<typeof useGetPollDetailsQu
return data;
}

const existingVote = data.poll_voters?.find((pv) => pv.name === activeUser!!.username);
const previousUserChoice = data.poll_choices?.find(
(pc) => existingVote?.choice_num === pc.choice_num
const existingVotes = data.poll_voters?.filter((pv) => pv.name === activeUser!!.username);
const previousUserChoices = data.poll_choices?.filter((pc) =>
existingVotes?.some((ev) => ev.choice_num === pc.choice_num)
);
const choice = data.poll_choices?.find((pc) => pc.choice_num === resp.choiceNum)!!;
const choices = data.poll_choices?.filter((pc) => !!resp.choiceNums[pc.choice_num]);

const notTouchedChoices = data.poll_choices?.filter(
(pc) => ![previousUserChoice?.choice_num, choice?.choice_num].includes(pc.choice_num)
(pc) =>
![
...previousUserChoices?.map((puc) => puc.choice_num),
choices?.map((c) => c.choice_num)
].includes(pc.choice_num)
);
const otherVoters =
data.poll_voters?.filter((pv) => pv.name !== activeUser!!.username) ?? [];
Expand All @@ -56,32 +62,34 @@ export function useSignPollVoteByKey(poll: ReturnType<typeof useGetPollDetailsQu
...data,
poll_choices: [
...notTouchedChoices,
previousUserChoice && previousUserChoice.choice_text !== choice.choice_text
? {
...previousUserChoice,
votes: {
total_votes: (previousUserChoice?.votes?.total_votes ?? 0) - 1
}
...(previousUserChoices
.filter((pv) => choices.every((c) => pv.choice_text !== c.choice_text))
.map((pv) => ({
...pv,
votes: {
total_votes: (pv?.votes?.total_votes ?? 0) - 1
}
: undefined,
{
})) ?? []),
...choices.map((choice) => ({
...choice,
votes: {
total_votes:
(choice?.votes?.total_votes ?? 0) +
(previousUserChoice?.choice_text !== choice.choice_text ? 1 : 0)
(previousUserChoices.every((pv) => pv.choice_text !== choice.choice_text)
? 1
: 0)
}
}
}))
].filter((el) => !!el),
poll_voters: [
...otherVoters,
{ name: activeUser?.username, choice_num: resp.choiceNum }
...resp.choiceNums.map((num) => ({ name: activeUser?.username, choice_num: num }))
],
poll_stats: {
...data.poll_stats,
total_voting_accounts_num: existingVote
? data.poll_stats.total_voting_accounts_num
: data.poll_stats.total_voting_accounts_num + 1
total_voting_accounts_num:
data.poll_stats.total_voting_accounts_num +
(choices.length - (existingVotes?.length ?? 0))
}
} as ReturnType<typeof useGetPollDetailsQuery>["data"];
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { PollSnapshot } from "./polls-creation";
import { _t } from "../../../i18n";

export interface Props {
activeChoice?: string;
activeChoices: Set<string>;
choice: string;
entry?: Entry;
interpretation: PollSnapshot["interpretation"];
}

export function PollOptionWithResults({ choice, activeChoice, entry, interpretation }: Props) {
export function PollOptionWithResults({ choice, activeChoices, entry, interpretation }: Props) {
const pollDetails = useGetPollDetailsQuery(entry);

const votesCount = useMemo(
Expand Down Expand Up @@ -61,7 +61,7 @@ export function PollOptionWithResults({ choice, activeChoice, entry, interpretat
width: `${progress}%`
}}
/>
{activeChoice === choice && <PollCheck checked={activeChoice === choice} />}
{activeChoices.has(choice) && <PollCheck checked={activeChoices.has(choice)} />}
<div className="flex w-full gap-2 justify-between">
<span>{choice}</span>
<span className="text-xs whitespace-nowrap">
Expand Down
15 changes: 8 additions & 7 deletions src/common/features/polls/components/poll-option.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,27 @@ export function PollCheck({ checked }: { checked: boolean }) {
}

export interface Props {
activeChoice?: string;
activeChoices: Set<string>;
choice: string;
setActiveChoice: (choice?: string) => void;
addActiveChoice: (choice: string) => void;
removeActiveChoice: (choice: string) => void;
}

export function PollOption({ activeChoice, choice, setActiveChoice }: Props) {
export function PollOption({ activeChoices, choice, addActiveChoice, removeActiveChoice }: Props) {
return (
<div
className={classNameObject({
"flex items-center gap-4 duration-300 cursor-pointer text-sm px-4 py-3 rounded-2xl": true,
"bg-gray-200 hover:bg-gray-300 dark:bg-dark-200 dark:hover:bg-gray-900":
activeChoice !== choice,
!activeChoices.has(choice),
"bg-blue-dark-sky hover:bg-blue-dark-sky-hover bg-opacity-50 hover:bg-opacity-50 text-blue-dark-sky-active dark:text-blue-dark-sky-010":
activeChoice === choice
activeChoices.has(choice)
})}
onClick={() =>
activeChoice === choice ? setActiveChoice(undefined) : setActiveChoice(choice)
activeChoices.has(choice) ? removeActiveChoice(choice) : addActiveChoice(choice)
}
>
<PollCheck checked={activeChoice === choice} />
<PollCheck checked={activeChoices.has(choice)} />
{choice}
</div>
);
Expand Down
22 changes: 15 additions & 7 deletions src/common/features/polls/components/poll-widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { format, isBefore } from "date-fns";
import useLocalStorage from "react-use/lib/useLocalStorage";
import { PREFIX } from "../../../util/local-storage";
import { FormControl } from "@ui/input";
import { useSet } from "react-use";

interface Props {
poll: PollSnapshot;
Expand All @@ -31,7 +32,7 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) {

const { mutateAsync: vote, isLoading: isVoting } = useSignPollVoteByKey(pollDetails.data);

const [activeChoice, setActiveChoice] = useState<string>();
const [activeChoices, { add: addActiveChoice, remove: removeActiveChoice }] = useSet<string>();
const [resultsMode, setResultsMode] = useState(false);
const [isVotedAlready, setIsVotedAlready] = useState(false);
const [showEndDate, setShowEndDate] = useLocalStorage(PREFIX + "_plls_set", false);
Expand Down Expand Up @@ -63,7 +64,9 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) {
const choice = pollDetails.data?.poll_choices.find(
(pc) => pc.choice_num === activeUserVote.choice_num
);
setActiveChoice(choice?.choice_text);
if (choice) {
addActiveChoice(choice?.choice_text);
}
}
}, [activeUserVote, pollDetails.data]);

Expand Down Expand Up @@ -132,14 +135,19 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) {
key={choice}
entry={entry}
choice={choice}
activeChoice={activeChoice}
activeChoices={activeChoices}
/>
) : (
<PollOption
choice={choice}
key={choice}
setActiveChoice={setActiveChoice}
activeChoice={activeChoice}
addActiveChoice={(v) => {
if (activeChoices.size < (pollDetails.data?.max_choices_voted ?? 1)) {
addActiveChoice(v);
}
}}
removeActiveChoice={removeActiveChoice}
activeChoices={activeChoices}
/>
)
)}
Expand All @@ -165,14 +173,14 @@ export function PollWidget({ poll, isReadOnly, entry }: Props) {
</div>
{showVote && (
<Button
disabled={isReadOnly || !activeChoice || isVoting}
disabled={isReadOnly || !activeChoices || isVoting}
icon={<UilPanelAdd />}
iconPlacement="left"
size="lg"
className="font-semibold text-sm px-4 mt-4"
onClick={() => {
setIsVotedAlready(false);
vote({ choice: activeChoice!! });
vote({ choices: activeChoices!! });
}}
>
{_t(isVoting ? "polls.voting" : "polls.vote")}
Expand Down
32 changes: 32 additions & 0 deletions src/common/features/polls/components/polls-creation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface PollSnapshot {
choices: string[];
voteChange: boolean;
hideVotes: boolean;
maxChoicesVoted: number;
filters: {
accountAge: number;
};
Expand Down Expand Up @@ -66,6 +67,8 @@ export function PollsCreation({
isExpiredEndDate,
endTime,
setEndTime,
maxChoicesVoted,
setMaxChoicesVoted,
clearAll
} = usePollsCreationManagement(existingPoll);

Expand Down Expand Up @@ -174,6 +177,34 @@ export function PollsCreation({
}
}}
/>
<div className="text-sm opacity-50">{_t("polls.max-choices-voted")}</div>
<div className="w-full flex items-center gap-2 justify-between">
<FormControl
placeholder="1"
type="number"
min={0}
max={choices?.length ?? 1}
value={maxChoicesVoted}
disabled={readonly}
onChange={(e) => {
const value = +e.target.value;
if (value >= 0 && value <= (choices?.length ?? 1)) {
setMaxChoicesVoted(+e.target.value);
} else if (value < 0) {
setMaxChoicesVoted(0);
} else {
setMaxChoicesVoted(choices?.length ?? 1);
}
}}
/>
<Button
size="sm"
onClick={() => setMaxChoicesVoted(choices?.length ?? 1)}
appearance="gray-link"
>
{_t("g.all")}
</Button>
</div>
<FormControl
disabled={readonly}
type="select"
Expand Down Expand Up @@ -258,6 +289,7 @@ export function PollsCreation({
choices,
voteChange: !!voteChange,
hideVotes: !!hideVotes,
maxChoicesVoted: maxChoicesVoted ?? 1,
filters: {
accountAge
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export function usePollsCreationManagement(poll?: PollSnapshot) {
useState<PollSnapshot["interpretation"]>("number_of_votes");
const [voteChange, setVoteChange] = useLocalStorage(PREFIX + "_plls_vc", true);
const [hideVotes, setHideVotes] = useLocalStorage(PREFIX + "_plls_cs", false);
const [maxChoicesVoted, setMaxChoicesVoted] = useLocalStorage(PREFIX + "_plls_mcv", 1);

const hasEmptyOrDuplicatedChoices = useMemo(() => {
if (!choices || choices.length <= 1) {
Expand Down Expand Up @@ -83,6 +84,8 @@ export function usePollsCreationManagement(poll?: PollSnapshot) {
isExpiredEndDate,
endTime,
setEndTime,
maxChoicesVoted,
setMaxChoicesVoted,
clearAll: () => {
clearTitle();
clearEndDate();
Expand Down
6 changes: 4 additions & 2 deletions src/common/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
"restore": "Restore",
"success": "Success",
"error": "Error",
"reset-form": "Reset form"
"reset-form": "Reset form",
"all": "All"
},
"confirm": {
"title": "Are you sure?",
Expand Down Expand Up @@ -2459,6 +2460,7 @@
"expired-date": "End date should be in present or future",
"interpretation": "Interpretation",
"creating-in-progress": "Creating in progress...",
"invalid-time": "Invalid time format. Use HH:MM"
"invalid-time": "Invalid time format. Use HH:MM",
"max-choices-voted": "Max choices voted by user"
}
}

0 comments on commit 2c0502c

Please sign in to comment.