aboutsummaryrefslogtreecommitdiffstats
path: root/src/pages/FormPage/FormPage.tsx
blob: a09b593a0305c808b17419c6e90dbf37922204ee (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
/** @jsx jsx */
/** @jsxFrag */
import {jsx, css} from "@emotion/react";

import React, {createRef, SyntheticEvent, useEffect, useState} from "react";
import {useParams} from "react-router";
import {PropagateLoader} from "react-spinners";
import {AxiosError} from "axios";

import HeaderBar from "../../components/HeaderBar";
import RenderedQuestion from "../../components/Question";
import Loading from "../../components/Loading";
import ScrollToTop from "../../components/ScrollToTop";

import {FormFeatures, FormWithAncillaryData, getForm} from "../../api/forms";
import {OAuthScopes} from "../../api/auth";
import {unselectable} from "../../commonStyles";

import handleSubmit, {FormState} from "./submit";
import Navigation from "./Navigation";
import Success from "./SuccessPage";
import ErrorPage from "./ErrorPage";
import NotFound from "../NotFound";
import PrecheckBox from "../../components/PrecheckBox";


export type RefMapType =  Map<string, React.RefObject<RenderedQuestion>>;


const formStyles = css`
  margin: auto;
  width: 50%;

  @media (max-width: 800px) {
    /* Make form larger on mobile and tablet screens */
    width: 80%;
  }
`;

enum LoadingState {
    Pending,
    Found,
    Missing
}

function FormPage(): JSX.Element {
    const {id} = useParams<{id: string}>();

    const [form, setForm] = useState<FormWithAncillaryData>();
    const [formLoading, setFormLoading] = useState<LoadingState>(LoadingState.Pending);
    const [state, setState] = useState<string>(FormState.INITIAL);

    const OAuthRef = createRef<HTMLDivElement>();

    useEffect(() => {
        // ID can't be null due to the routing to get here
        getForm(id!).then(form => {
            setForm(form);
            setFormLoading(LoadingState.Found);
        }).catch((error: AxiosError) => {
            if (error.response?.status === 404) {
                setFormLoading(LoadingState.Missing);
                return;
            }

            throw error;
        });
    }, []);

    switch (formLoading) {
        case LoadingState.Pending:
            return <Loading/>;
        case LoadingState.Missing:
            return <NotFound message={"Could not find a matching form. Make sure the requested form exists and is open."}/>;
    }

    if (!form) {
        // This should be an impossible state
        throw Error("Form was not set despite loading state being set to found.");
    }

    const refMap: RefMapType = new Map();

    // Authentication Logic
    const require_auth: boolean = form.features.includes(FormFeatures.RequiresLogin);
    const scopes: OAuthScopes[] = [];
    if (require_auth) {
        scopes.push(OAuthScopes.Identify);
        if (form.features.includes(FormFeatures.CollectEmail)) {
            scopes.push(OAuthScopes.Email);
        }
    }

    const prechecks: JSX.Element[] = [];
    for (const precheck of form.submission_precheck.problems) {
        prechecks.push(<PrecheckBox message={precheck.message} severity={precheck.severity} key={precheck.message}/>);
    }

    const questions: RenderedQuestion[] = form.questions.map((question, index) => {
        const questionRef = createRef<RenderedQuestion>();
        refMap.set(question.id, questionRef);

        return <RenderedQuestion
            question={question}
            scroll_ref={createRef<HTMLDivElement>()}
            focus_ref={createRef<any>()} // eslint-disable-line @typescript-eslint/no-explicit-any
            key={index + Date.now()}
            selfRef={questionRef}
            ref={questionRef}
        /> as unknown as RenderedQuestion; // Annotations for JSX elements resolve to a generic ReactElement
    });

    switch (state) {
        case FormState.SENT:
            return <Success form={form}/>;
        case FormState.SENDING:
            return (
                <div>
                    <HeaderBar title={"Submitting..."}/>
                    <div css={{display: "flex", justifyContent: "center", paddingTop: "40px"}}>
                        <PropagateLoader color="white"/>
                    </div>
                </div>
            );

        case FormState.UNKNOWN_ERROR:
            return <ErrorPage
                form={form}
                message="An unknown error occurred, please contact the forms team or try again."
            />;
    }

    const handler = (event: SyntheticEvent) => handleSubmit(event, form.id, questions, refMap, setState, OAuthRef, scopes);

    return (
        <div>
            <HeaderBar title={form.name} description={form.description}/>

            <div>
                <form id="form" onSubmit={handler} css={[formStyles, unselectable]}>
                    <>
                        {...prechecks}
                        {questions}
                    </>
                </form>
                <Navigation can_submit={form.submission_precheck.can_submit} scopes={scopes}/>
            </div>

            <div css={css`margin-bottom: 10rem`}/>
            <ScrollToTop/>
        </div>
    );
}

export default FormPage;