diff options
| -rw-r--r-- | src/pages/FormPage.tsx | 52 | ||||
| -rw-r--r-- | src/store/form/actions.ts | 14 | ||||
| -rw-r--r-- | src/store/form/reducers.ts | 7 | ||||
| -rw-r--r-- | src/store/form/types.ts | 3 | 
4 files changed, 69 insertions, 7 deletions
diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index e9306e3..c06b33c 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -2,10 +2,11 @@  import { jsx, css } from "@emotion/react";  import { Link } from "react-router-dom"; -import React, { SyntheticEvent, useEffect, useState, createRef } from "react"; +import React, {SyntheticEvent, useEffect, useState, createRef, useRef} from "react";  import { useParams } from "react-router";  import { PropagateLoader } from "react-spinners";  import { useDispatch, useSelector } from "react-redux"; +import HCaptcha from "@hcaptcha/react-hcaptcha";  import HeaderBar from "../components/HeaderBar";  import ConnectedRenderedQuestion, { RenderedQuestion } from "../components/Question"; @@ -18,7 +19,7 @@ import { unselectable }  from "../commonStyles";  import { Question, QuestionType } from "../api/question";  import ApiClient from "../api/client";  import { FormState } from "../store/form/types"; -import { clean } from "../store/form/actions"; +import { clean, setCaptchaToken } from "../store/form/actions";  interface PathParams {      id: string @@ -157,6 +158,14 @@ const closedHeaderStyles = css`    }  `; +const captchaStyles = css` +  text-align: center; +   +  @media (max-width: 850px) { +    padding-bottom: 1.2rem; +  } +`; +  function FormPage(): JSX.Element {      const { id } = useParams<PathParams>(); @@ -166,6 +175,9 @@ function FormPage(): JSX.Element {      const values = useSelector<FormState, FormState["values"]>(          state => state.values      ); +    const captchaToken = useSelector<FormState, FormState["captchaToken"]>( +        state => state.captchaToken +    );      const dispatch = useDispatch(); @@ -173,6 +185,9 @@ function FormPage(): JSX.Element {      const [sending, setSending] = useState<boolean>();      const [sent, setSent] = useState<boolean>(); +    // eslint-disable-next-line @typescript-eslint/no-explicit-any +    const captchaRef = useRef<any>(); +      useEffect(() => {          dispatch(clean());          getForm(id).then(form => { @@ -255,6 +270,14 @@ function FormPage(): JSX.Element {              return;          } +        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion +        if (!(FormFeatures.DisableAntispam in form!.features) && captchaToken === null) { +            if (captchaRef && captchaRef.current) { +                captchaRef.current.execute(); +            } +            return; +        } +          setSending(true);          const answers: { [key: string]: unknown } = {}; @@ -294,7 +317,14 @@ function FormPage(): JSX.Element {              }          }); -        await ApiClient.post(`forms/submit/${id}`, {response: answers}); +        const data: { [key: string]: unknown } = {response: answers}; + +        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion +        if (!(FormFeatures.DisableAntispam in form!.features)) { +            data["captcha"] = captchaToken; +        } + +        await ApiClient.post(`forms/submit/${id}`, data);          setSending(false);          setSent(true);      } @@ -306,6 +336,21 @@ function FormPage(): JSX.Element {          closed_header = <div css={closedHeaderStyles}>This form is now closed. You will not be able to submit your response.</div>;      } +    let captcha = null; +    if (!(FormFeatures.DisableAntispam in form.features)) { +        captcha = ( +            <div css={captchaStyles}> +                <HCaptcha +                    sitekey={process.env.HCAPTCHA_SITEKEY ? process.env.HCAPTCHA_SITEKEY : ""} +                    theme={"dark"} +                    onVerify={(token) => dispatch(setCaptchaToken(token))} +                    onExpire={() => dispatch(setCaptchaToken(null))} +                    ref={captchaRef} +                /> +            </div> +        ); +    } +      return (          <div>              <HeaderBar title={form.name} description={form.description}/> @@ -314,6 +359,7 @@ function FormPage(): JSX.Element {                  <form id="form" onSubmit={handleSubmit} css={[formStyles, unselectable]}>                      { closed_header }                      { questions } +                    { captcha }                  </form>                  <Navigation form_state={open}/>              </div> diff --git a/src/store/form/actions.ts b/src/store/form/actions.ts index 8b48a37..dc43729 100644 --- a/src/store/form/actions.ts +++ b/src/store/form/actions.ts @@ -5,7 +5,8 @@ export enum FormAction {      SET_VALUE = "SET_VALUE",      SET_ERROR = "SET_ERROR",      SET_VALID = "SET_VALID", -    CLEAN = "CLEAN" +    CLEAN = "CLEAN", +    SET_CAPTCHA_TOKEN = "SET_CAPTCHA_TOKEN"  }  // This is base for all actions @@ -42,7 +43,12 @@ export interface CleanAction extends DefaultFormAction {      type: FormAction.CLEAN  } -export type Action = SetValueAction | SetErrorAction | SetValidAction | CleanAction; +export interface SetCaptchaTokenAction extends DefaultFormAction { +    type: FormAction.SET_CAPTCHA_TOKEN, +    payload: string | null +} + +export type Action = SetValueAction | SetErrorAction | SetValidAction | CleanAction | SetCaptchaTokenAction;  export function setValue(question: Question, value: string | Map<string, boolean> | null): SetValueAction {      return { @@ -77,3 +83,7 @@ export function setValid(question: Question, valid: boolean): SetValidAction {  export function clean(): CleanAction {      return { type: FormAction.CLEAN };  } + +export function setCaptchaToken(token: string | null): SetCaptchaTokenAction { +    return { type: FormAction.SET_CAPTCHA_TOKEN, payload: token }; +} diff --git a/src/store/form/reducers.ts b/src/store/form/reducers.ts index fcbb33a..21bd047 100644 --- a/src/store/form/reducers.ts +++ b/src/store/form/reducers.ts @@ -4,7 +4,8 @@ import {FormState} from "./types";  export const initialState: FormState = {      values: new Map(),      errors: new Map(), -    valid: new Map() +    valid: new Map(), +    captchaToken: null  };  export function formReducer(state = initialState, action: Action): FormState { @@ -24,6 +25,10 @@ export function formReducer(state = initialState, action: Action): FormState {          case FormAction.CLEAN:              return initialState; + +        case FormAction.SET_CAPTCHA_TOKEN: +            new_state.captchaToken = action.payload; +            break;      }      return new_state;  } diff --git a/src/store/form/types.ts b/src/store/form/types.ts index 1d3e594..3f8557e 100644 --- a/src/store/form/types.ts +++ b/src/store/form/types.ts @@ -1,5 +1,6 @@  export interface FormState {      values: Map<string, string | Map<string, boolean> | null>,      errors: Map<string, string>, -    valid: Map<string, boolean> +    valid: Map<string, boolean>, +    captchaToken: string | null  }  |