From 58310afba4e89db6966fbf8f32cd4a44a52a6ca6 Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Thu, 11 Jul 2024 02:23:51 +0100
Subject: Add new vote component and voting redux store
---
src/components/InputTypes/Vote.tsx | 226 +++++++++++++++++++++++++++++++++++++
src/slices/votes.ts | 73 ++++++++++++
src/utils.ts | 24 ++++
3 files changed, 323 insertions(+)
create mode 100644 src/components/InputTypes/Vote.tsx
create mode 100644 src/slices/votes.ts
create mode 100644 src/utils.ts
(limited to 'src')
diff --git a/src/components/InputTypes/Vote.tsx b/src/components/InputTypes/Vote.tsx
new file mode 100644
index 0000000..67a7cac
--- /dev/null
+++ b/src/components/InputTypes/Vote.tsx
@@ -0,0 +1,226 @@
+/** @jsx jsx */
+import { jsx, css } from "@emotion/react";
+
+import React, { useEffect, useState } from "react";
+import RenderedQuestion from "../Question";
+import { slugify, humanize } from "../../utils";
+import colors from "../../colors";
+import { useDispatch, useSelector } from "react-redux";
+import { registerVote, changeVote, VoteSliceState } from "../../slices/votes";
+import FlipMove from "react-flip-move";
+
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCaretUp, faCaretDown } from "@fortawesome/free-solid-svg-icons";
+
+interface VoteProps {
+ options: Array;
+ valid: boolean;
+ question: React.RefObject;
+ questionId: string;
+ handler: (event: Record) => void;
+}
+
+const baseCardStyles = css`
+ display: flex;
+ align-items: center;
+ background-color: ${colors.darkerGreyple};
+ padding: 10px;
+ border-radius: 5px;
+ margin-bottom: 10px;
+`;
+
+const buttonStyles = css`
+ border: none;
+ background: ${colors.greyple};
+ cursor: pointer;
+ margin-right: 20px;
+ margin-left: 20px;
+ border-radius: 5px;
+ transition: transform 0.1s ease;
+ transform: none;
+
+ :hover {
+ transform: scale(1.1);
+ }
+`;
+
+const iconStyles = css`
+ font-size: 2em;
+ color: white;
+`;
+
+const markerStyles = css`
+ border-radius: 5px;
+ padding: 5px;
+ margin-right: 10px;
+`;
+
+const votingStyles = css`
+ button {
+ background-color: ${colors.darkerBlurple};
+ }
+`;
+
+const Buttons = (base: { questionId: string; optionId: string }) => {
+ const dispatch = useDispatch();
+
+ return (
+
+ dispatch(changeVote({ ...base, change: -1 }))}
+ >
+
+
+ dispatch(changeVote({ ...base, change: +1 }))}
+ >
+
+
+
+ );
+};
+
+const Card = ({
+ id,
+ content,
+ questionId,
+}: {
+ id: string;
+ content: string;
+ questionId: string;
+}) => {
+ const foundIndex = useSelector<{ vote: VoteSliceState }, number | null>(
+ (state) => {
+ const votes = state.vote?.votes;
+ const questionVotes = votes?.[questionId];
+ return questionVotes ? questionVotes[id] : null;
+ },
+ );
+
+ const indexMarker =
+ foundIndex === null ? (
+
+ No preference
+
+ ) : (
+
+ {humanize(foundIndex)}
+
+ );
+
+ return (
+
+ {indexMarker}
+
{content}
+
+
+
+ );
+};
+
+const CardList = React.memo(function CardList({
+ cards,
+ questionId,
+ handler,
+}: {
+ cards: string[];
+ questionId: string;
+ handler: VoteProps["handler"];
+}) {
+ const votes = useSelector<
+ { vote: VoteSliceState },
+ Record
+ >((state) => {
+ const votes = state.vote?.votes;
+ const questionVotes = votes?.[questionId];
+ return questionVotes;
+ });
+
+ useEffect(() => {
+ handler(votes);
+ }, [votes]);
+
+ if (votes) {
+ cards = cards.sort((a, b) => {
+ if (!votes[slugify(a)]) {
+ return 1;
+ }
+
+ if (!votes[slugify(b)]) {
+ return -1;
+ }
+ return votes[slugify(a)] - votes[slugify(b)];
+ });
+ }
+
+ return (
+
+ {cards.map((cardContent: string) => (
+
+
+
+ ))}
+
+ );
+});
+
+export default function Vote(props: VoteProps): JSX.Element {
+ const [state,] = useState({
+ cards: props.options
+ .map((value) => ({ value, sort: Math.random() }))
+ .sort((a, b) => a.sort - b.sort)
+ .map(({ value }) => value),
+ });
+
+ const dispatch = useDispatch();
+
+ useEffect(() => {
+ dispatch(
+ registerVote({
+ questionId: props.questionId,
+ questionSlugs: state.cards.map((value) => slugify(value)),
+ }),
+ );
+ }, [props.questionId]);
+
+ const COPY = "Use the buttons to organise options into your preferred order. You can have multiple options with the same ranking. Additionally, you can leave some or all options as \"No preference\" if you do not wish to order them.";
+
+ return (
+
+ );
+}
diff --git a/src/slices/votes.ts b/src/slices/votes.ts
new file mode 100644
index 0000000..87d84e8
--- /dev/null
+++ b/src/slices/votes.ts
@@ -0,0 +1,73 @@
+import { createSlice, PayloadAction } from "@reduxjs/toolkit";
+
+export interface VoteSliceState {
+ votes: Record>;
+}
+
+interface VoteChange {
+ questionId: string;
+ optionId: string;
+ change: 1 | -1;
+}
+
+interface VoteRegister {
+ questionId: string;
+ questionSlugs: string[];
+}
+
+const voteSlice = createSlice({
+ name: "vote",
+ initialState: {
+ votes: {},
+ },
+ reducers: {
+ registerVote: (
+ state: VoteSliceState,
+ action: PayloadAction,
+ ) => {
+ state.votes[action.payload.questionId] = {};
+ action.payload.questionSlugs.forEach((value) => {
+ state.votes[action.payload.questionId][value] = null;
+ });
+ },
+ changeVote: (
+ state: VoteSliceState,
+ action: PayloadAction,
+ ) => {
+ const foundVote =
+ state.votes[action.payload.questionId][action.payload.optionId];
+
+ if (foundVote !== null) {
+ if (foundVote === 1 && action.payload.change <= 0) {
+ return;
+ }
+ if (
+ foundVote >=
+ Object.keys(state.votes[action.payload.questionId])
+ .length &&
+ action.payload.change >= 0
+ ) {
+ state.votes[action.payload.questionId][
+ action.payload.optionId
+ ] = null;
+ return;
+ }
+ state.votes[action.payload.questionId][
+ action.payload.optionId
+ ] += action.payload.change;
+ } else {
+ if (action.payload.change <= 0) {
+ state.votes[action.payload.questionId][
+ action.payload.optionId
+ ] = Object.keys(
+ state.votes[action.payload.questionId],
+ ).length;
+ }
+ }
+ },
+ },
+});
+
+export const { registerVote, changeVote } = voteSlice.actions;
+
+export default voteSlice.reducer;
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..8067045
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,24 @@
+export function slugify(str: string) {
+ str = str.replace(/^\s+|\s+$/g, "");
+ str = str.toLowerCase();
+ str = str
+ .replace(/[^a-z0-9 -]/g, "")
+ .replace(/\s+/g, "-")
+ .replace(/-+/g, "-");
+ return str;
+}
+
+export function humanize(num: number) {
+ if (num % 100 >= 11 && num % 100 <= 13) return num + "th";
+
+ switch (num % 10) {
+ case 1:
+ return num + "st";
+ case 2:
+ return num + "nd";
+ case 3:
+ return num + "rd";
+ }
+
+ return num + "th";
+}
--
cgit v1.2.3
From 572454f01749b4e8b43bfd9c1fcc47210d86fa0f Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Thu, 11 Jul 2024 02:24:08 +0100
Subject: Handle voting component callbacks
---
src/api/question.ts | 1 +
src/components/InputTypes/index.tsx | 5 +++++
src/components/Question.tsx | 18 +++++++++++++++---
3 files changed, 21 insertions(+), 3 deletions(-)
(limited to 'src')
diff --git a/src/api/question.ts b/src/api/question.ts
index 56706e2..0fcea03 100644
--- a/src/api/question.ts
+++ b/src/api/question.ts
@@ -8,6 +8,7 @@ export enum QuestionType {
Range = "range",
Section = "section",
TimeZone = "timezone",
+ Vote = "vote"
}
export interface Question {
diff --git a/src/components/InputTypes/index.tsx b/src/components/InputTypes/index.tsx
index ee92ef1..1880bcd 100644
--- a/src/components/InputTypes/index.tsx
+++ b/src/components/InputTypes/index.tsx
@@ -11,6 +11,7 @@ import {QuestionType} from "../../api/question";
import RenderedQuestion from "../Question";
import Code from "./Code";
import TimeZone from "./TimeZone";
+import Vote from "./Vote";
export default function create_input(
{props: renderedQuestionProps, realState}: RenderedQuestion,
@@ -67,6 +68,10 @@ export default function create_input(
result = ;
break;
+ case QuestionType.Vote:
+ result = ;
+ break;
+
default:
result =
}
- showDialog={true}
- dialogOptions={{
- title: "You've found a bug in PyDis forms!"
- }}
- onError={(err) => {
- if(process.env.NODE_ENV === "development")
- console.log(err);
- }}
- >
-
-
-
-
-
+ An error has occurred with Python Discord Forms. Please let us know in the Discord server at discord.gg/python }
+ showDialog={true}
+ dialogOptions={{
+ title: "You've found a bug in PyDis forms!"
+ }}
+ onError={(err) => {
+ if(process.env.NODE_ENV === "development")
+ console.log(err);
+ }}
+ >
+
+
+
+
);
serviceWorker.unregister();
--
cgit v1.2.3
From f404479d2aea3498ff1bb8b31f3e24de39f9b0bd Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Thu, 11 Jul 2024 02:24:27 +0100
Subject: Add voteReducer to rootReducer Redux configuration
---
src/store.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
(limited to 'src')
diff --git a/src/store.ts b/src/store.ts
index 1b9807b..e516bba 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -1,9 +1,11 @@
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import authorizationReducer from "./slices/authorization";
+import voteReducer from "./slices/votes";
const rootReducer = combineReducers({
- authorization: authorizationReducer
+ authorization: authorizationReducer,
+ vote: voteReducer
});
export const setupStore = (preloadedState?: Partial) => {
--
cgit v1.2.3
From dcf24138d153f3b90ee30e3ae3901b9f96943eed Mon Sep 17 00:00:00 2001
From: Joe Banks
Date: Thu, 11 Jul 2024 03:00:17 +0100
Subject: Re-map slugged vote options to human form when upstreaming to form
---
src/components/InputTypes/Vote.tsx | 17 ++++++++++++++++-
1 file changed, 16 insertions(+), 1 deletion(-)
(limited to 'src')
diff --git a/src/components/InputTypes/Vote.tsx b/src/components/InputTypes/Vote.tsx
index 67a7cac..c630d8c 100644
--- a/src/components/InputTypes/Vote.tsx
+++ b/src/components/InputTypes/Vote.tsx
@@ -146,10 +146,12 @@ const CardList = React.memo(function CardList({
cards,
questionId,
handler,
+ reverseMap
}: {
cards: string[];
questionId: string;
handler: VoteProps["handler"];
+ reverseMap: Record;
}) {
const votes = useSelector<
{ vote: VoteSliceState },
@@ -161,7 +163,15 @@ const CardList = React.memo(function CardList({
});
useEffect(() => {
- handler(votes);
+ if (!votes) {
+ return;
+ }
+
+ const updated = Object.fromEntries(
+ Object.entries(votes).map(([slug, vote]) => [reverseMap[slug], vote])
+ );
+
+ handler(updated);
}, [votes]);
if (votes) {
@@ -211,6 +221,10 @@ export default function Vote(props: VoteProps): JSX.Element {
);
}, [props.questionId]);
+ const reverseMap = Object.fromEntries(props.options.map(value => {
+ return [slugify(value), value];
+ }));
+
const COPY = "Use the buttons to organise options into your preferred order. You can have multiple options with the same ranking. Additionally, you can leave some or all options as \"No preference\" if you do not wish to order them.";
return (
@@ -220,6 +234,7 @@ export default function Vote(props: VoteProps): JSX.Element {
questionId={props.questionId}
handler={props.handler}
cards={state.cards}
+ reverseMap={reverseMap}
/>
);
--
cgit v1.2.3