Skip to content

Migrate to Redux and implement hCaptcha #148

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@fortawesome/free-brands-svg-icons": "5.15.2",
"@fortawesome/free-solid-svg-icons": "5.15.2",
"@fortawesome/react-fontawesome": "0.1.14",
"@hcaptcha/react-hcaptcha": "^0.3.2",
"@sentry/react": "6.1.0",
"@svgr/webpack": "5.5.0",
"@swc/core": "1.2.47",
Expand All @@ -20,9 +21,11 @@
"react": "17.0.1",
"react-app-polyfill": "2.0.0",
"react-dom": "17.0.1",
"react-redux": "^7.2.2",
"react-router-dom": "5.2.0",
"react-spinners": "0.10.4",
"react-transition-group": "4.4.1",
"redux": "^4.0.5",
"smoothscroll-polyfill": "0.4.4",
"swc-loader": "0.1.12",
"typescript": "4.1.5",
Expand Down Expand Up @@ -57,12 +60,15 @@
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "11.2.5",
"@testing-library/user-event": "12.6.3",
"@types/hcaptcha__react-hcaptcha": "^0.1.4",
"@types/jest": "26.0.20",
"@types/node": "14.14.25",
"@types/react": "17.0.1",
"@types/react-dom": "17.0.0",
"@types/react-redux": "^7.1.16",
"@types/react-router-dom": "5.1.7",
"@types/react-transition-group": "4.4.0",
"@types/redux": "^3.6.0",
"@types/smoothscroll-polyfill": "0.3.1",
"@typescript-eslint/eslint-plugin": "4.15.0",
"@typescript-eslint/parser": "4.15.0",
Expand Down
4 changes: 3 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import React, { Suspense } from "react";
import { jsx, css, Global } from "@emotion/react";

import { Provider } from "react-redux";
import {
BrowserRouter as Router,
Route,
Expand All @@ -14,6 +15,7 @@ import { PropagateLoader } from "react-spinners";
import { CSSTransition, TransitionGroup } from "react-transition-group";

import globalStyles from "./globalStyles";
import { store } from "./store/form/store";

const LandingPage = React.lazy(() => import("./pages/LandingPage"));
const FormPage = React.lazy(() => import("./pages/FormPage"));
Expand Down Expand Up @@ -51,7 +53,7 @@ function App(): JSX.Element {
{routes.map(({path, Component}) => (
<Route exact key={path} path={path}>
<Suspense fallback={<PageLoading/>}>
<Component/>
{path == "/form/:id" ? <Provider store={store}><Component/></Provider> : <Component/>}
</Suspense>
</Route>
))}
Expand Down
19 changes: 16 additions & 3 deletions src/components/InputTypes/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { jsx, css } from "@emotion/react";
import React, { ChangeEvent } from "react";
import colors from "../../colors";
import { multiSelectInput, hiddenInput } from "../../commonStyles";
import { useSelector } from "react-redux";
import { FormState } from "../../store/form/types";
import { Question } from "../../api/question";

interface CheckboxProps {
index: number,
option: string,
handler: (event: ChangeEvent<HTMLInputElement>) => void
handler: (event: ChangeEvent<HTMLInputElement>) => void,
question: Question
}

const generalStyles = css`
Expand Down Expand Up @@ -53,10 +57,19 @@ const activeStyles = css`
`;

export default function Checkbox(props: CheckboxProps): JSX.Element {
const values = useSelector<FormState, FormState["values"]>(
state => state.values
);
let value = values[props.question.id];
if (typeof value !== "object" || !value) {
value = {};
}
const checked = value[`${("000" + props.index).slice(-4)}. ${props.option}`];

return (
<label css={[generalStyles, activeStyles]}>
<label className="unselected" css={multiSelectInput}>
<input type="checkbox" value={props.option} css={hiddenInput} name={`${("000" + props.index).slice(-4)}. ${props.option}`} onChange={props.handler}/>
<label className={checked ? "selected" : "unselected"} css={multiSelectInput}>
<input type="checkbox" value={props.option} css={hiddenInput} name={`${("000" + props.index).slice(-4)}. ${props.option}`} onChange={props.handler} checked={checked}/>
<span className="checkmark"/>
</label>
{props.option}<br/>
Expand Down
8 changes: 7 additions & 1 deletion src/components/InputTypes/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { jsx, css } from "@emotion/react";
import React, { ChangeEvent } from "react";
import colors from "../../colors";
import { multiSelectInput, hiddenInput } from "../../commonStyles";
import { useSelector } from "react-redux";
import { FormState } from "../../store/form/types";

interface RadioProps {
option: string,
Expand Down Expand Up @@ -30,9 +32,13 @@ const styles = css`
`;

export default function Radio(props: RadioProps): JSX.Element {
const values = useSelector<FormState, FormState["values"]>(
state => state.values
);
const value = values[props.question_id];
return (
<label css={styles}>
<input type="radio" name={props.question_id} onChange={props.handler} css={hiddenInput} onBlur={props.onBlurHandler}/>
<input type="radio" name={props.question_id} onChange={props.handler} css={hiddenInput} onBlur={props.onBlurHandler} checked={value === props.option}/>
<div css={multiSelectInput}/>
{props.option}<br/>
</label>
Expand Down
8 changes: 7 additions & 1 deletion src/components/InputTypes/Range.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { jsx, css } from "@emotion/react";
import React, { ChangeEvent } from "react";
import colors from "../../colors";
import { hiddenInput, multiSelectInput } from "../../commonStyles";
import { useSelector } from "react-redux";
import { FormState } from "../../store/form/types";

interface RangeProps {
question_id: string,
Expand Down Expand Up @@ -97,11 +99,15 @@ const sliderStyles = css`
`;

export default function Range(props: RangeProps): JSX.Element {
const values = useSelector<FormState, FormState["values"]>(
state => state.values
);
const value = values[props.question_id];
const range = props.options.map((option, index) => {
return (
<label css={[selectorStyles, css`width: 1rem`]} key={index}>
<span css={optionStyles}>{option}</span>
<input type="radio" name={props.question_id} css={hiddenInput} onChange={props.handler} onBlur={props.onBlurHandler}/>
<input type="radio" name={props.question_id} css={hiddenInput} onChange={props.handler} onBlur={props.onBlurHandler} checked={value === option}/>
<div css={multiSelectInput}/>
</label>
);
Expand Down
40 changes: 32 additions & 8 deletions src/components/InputTypes/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
/** @jsx jsx */
import { jsx, css } from "@emotion/react";
import React from "react";
import { connect } from "react-redux";

import { hiddenInput, invalidStyles } from "../../commonStyles";
import { Question } from "../../api/question";
import { setValue, SetValueAction } from "../../store/form/actions";
import { FormState } from "../../store/form/types";

interface SelectProps {
options: Array<string>,
state_dict: Map<string, string | boolean | null>,
valid: boolean,
onBlurHandler: () => void
onBlurHandler: () => void,
question: Question
}

interface SelectStateProps {
values: { [key: string]: string | { [subKey: string]: boolean; } | null; }
}

interface SelectDispatchProps {
setValue: (question: Question, value: string | { [key: string]: boolean } | null) => SetValueAction
}

const containerStyles = css`
Expand Down Expand Up @@ -143,15 +156,15 @@ const optionStyles = css`
}
`;

class Select extends React.Component<SelectProps> {
class Select extends React.Component<SelectProps & SelectStateProps & SelectDispatchProps> {
handler(selected_option: React.RefObject<HTMLDivElement>, event: React.ChangeEvent<HTMLInputElement>): void {
const option_container = event.target.parentElement;
if (!option_container || !option_container.parentElement || !selected_option.current) {
return;
}

// Update stored value
this.props.state_dict.set("value", option_container.textContent);
this.props.setValue(this.props.question, option_container.textContent);

// Close the menu
selected_option.current.focus();
Expand All @@ -178,10 +191,10 @@ class Select extends React.Component<SelectProps> {
}

focusOption(): void {
if (!this.props.state_dict.get("value")) {
this.props.state_dict.set("value", "temporary");
if (!(this.props.question.id in this.props.values) || !this.props.values[this.props.question.id]) {
this.props.setValue(this.props.question, "temporary");
this.props.onBlurHandler();
this.props.state_dict.set("value", null);
this.props.setValue(this.props.question, null);
}
}

Expand Down Expand Up @@ -211,4 +224,15 @@ class Select extends React.Component<SelectProps> {
}
}

export default Select;
const mapStateToProps = (state: FormState, ownProps: SelectProps): SelectProps & SelectStateProps => {
return {
...ownProps,
values: state.values
};
};

const mapDispatchToProps = {
setValue
};

export default connect(mapStateToProps, mapDispatchToProps)(Select);
14 changes: 12 additions & 2 deletions src/components/InputTypes/ShortText.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
/** @jsx jsx */
import { jsx } from "@emotion/react";
import React, { ChangeEvent } from "react";
import { useSelector } from "react-redux";

import { textInputs, invalidStyles } from "../../commonStyles";
import { Question } from "../../api/question";
import { FormState } from "../../store/form/types";

interface ShortTextProps {
handler: (event: ChangeEvent<HTMLInputElement>) => void,
onBlurHandler: () => void,
valid: boolean,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
focus_ref: React.RefObject<any>
focus_ref: React.RefObject<any>,
question: Question
}

export default function ShortText(props: ShortTextProps): JSX.Element {
const values = useSelector<FormState, FormState["values"]>(
state => state.values
);
const value = values[props.question.id];

return (
<div css={invalidStyles}>
<input type="text" css={textInputs} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
<input type="text" value={typeof value === "string" ? value : ""} css={textInputs} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
</div>
);
}
12 changes: 10 additions & 2 deletions src/components/InputTypes/TextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@
import { jsx, css } from "@emotion/react";
import React, { ChangeEvent } from "react";
import { invalidStyles, textInputs } from "../../commonStyles";
import { useSelector } from "react-redux";
import { FormState } from "../../store/form/types";
import { Question } from "../../api/question";

interface TextAreaProps {
handler: (event: ChangeEvent<HTMLTextAreaElement>) => void,
onBlurHandler: () => void,
valid: boolean,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
focus_ref: React.RefObject<any>
focus_ref: React.RefObject<any>,
question: Question
}

const styles = css`
Expand All @@ -21,9 +25,13 @@ const styles = css`
`;

export default function TextArea(props: TextAreaProps): JSX.Element {
const values = useSelector<FormState, FormState["values"]>(
state => state.values
);
const value = values[props.question.id];
return (
<div css={invalidStyles}>
<textarea css={[textInputs, styles]} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
<textarea css={[textInputs, styles]} value={typeof value === "string" ? value : ""} placeholder="Enter Text..." onChange={props.handler} onBlur={props.onBlurHandler} className={!props.valid ? "invalid-box" : ""} ref={props.focus_ref}/>
</div>
);
}
24 changes: 11 additions & 13 deletions src/components/InputTypes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import Select from "./Select";
import ShortText from "./ShortText";
import TextArea from "./TextArea";

import React, { ChangeEvent } from "react";
import React, {ChangeEvent} from "react";

import { QuestionType } from "../../api/question";
import { QuestionProp } from "../Question";
import {QuestionType} from "../../api/question";
import {QuestionDispatchProp, QuestionProp, QuestionStateProp} from "../Question";

const require_options: Array<QuestionType> = [
QuestionType.Radio,
Expand All @@ -19,16 +19,14 @@ const require_options: Array<QuestionType> = [
];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function create_input({ question, public_state }: QuestionProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, onBlurHandler: () => void, focus_ref: React.RefObject<any>): JSX.Element | JSX.Element[] {
export default function create_input(props: QuestionProp & QuestionStateProp & QuestionDispatchProp, handler: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void, onBlurHandler: () => void, focus_ref: React.RefObject<any>): JSX.Element | JSX.Element[] {
let result: JSX.Element | JSX.Element[];
const question = props.question;

// eslint-disable-next-line
// @ts-ignore
let options: string[] = question.data["options"];
let valid = true;
if (!public_state.get("valid")) {
valid = false;
}
const valid = props.valid[question.id];

// Catch input types that require options but don't have any
if ((options === undefined || typeof options !== "object") && require_options.includes(question.type)) {
Expand All @@ -39,23 +37,23 @@ export default function create_input({ question, public_state }: QuestionProp, h
/* eslint-disable react/react-in-jsx-scope */
switch (question.type) {
case QuestionType.TextArea:
result = <TextArea handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
result = <TextArea question={question} handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
break;

case QuestionType.Checkbox:
result = options.map((option, index) => <Checkbox index={index} option={option} handler={handler} key={index}/>);
result = options.map((option, index) => <Checkbox question={question} index={index} option={option} handler={handler} key={index}/>);
break;

case QuestionType.Radio:
result = options.map((option, index) => <Radio option={option} question_id={question.id} handler={handler} key={index} onBlurHandler={onBlurHandler}/>);
break;

case QuestionType.Select:
result = <Select options={options} state_dict={public_state} valid={valid} onBlurHandler={onBlurHandler}/>;
result = <Select options={options} question={question} valid={valid} onBlurHandler={onBlurHandler}/>;
break;

case QuestionType.ShortText:
result = <ShortText handler={handler} onBlurHandler={onBlurHandler} valid={valid} focus_ref={focus_ref}/>;
result = <ShortText key={props.question.id} handler={handler} onBlurHandler={onBlurHandler} valid={valid} focus_ref={focus_ref} question={question}/>;
break;

case QuestionType.Range:
Expand All @@ -68,7 +66,7 @@ export default function create_input({ question, public_state }: QuestionProp, h
break;

default:
result = <TextArea handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
result = <TextArea question={question} handler={handler} valid={valid} onBlurHandler={onBlurHandler} focus_ref={focus_ref}/>;
}
/* eslint-enable react/react-in-jsx-scope */

Expand Down
Loading