aboutsummaryrefslogtreecommitdiffstats
path: root/src/pages/FormPage.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages/FormPage.tsx')
-rw-r--r--src/pages/FormPage.tsx347
1 files changed, 0 insertions, 347 deletions
diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx
deleted file mode 100644
index f21e4f0..0000000
--- a/src/pages/FormPage.tsx
+++ /dev/null
@@ -1,347 +0,0 @@
-/** @jsx jsx */
-import { jsx, css } from "@emotion/react";
-import { Link } from "react-router-dom";
-
-import React, { SyntheticEvent, useEffect, useState, createRef } 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 OAuth2Button from "../components/OAuth2Button";
-
-import { Form, FormFeatures, getForm } from "../api/forms";
-import { OAuthScopes, checkScopes } from "../api/auth";
-import colors from "../colors";
-import { submitStyles, unselectable } from "../commonStyles";
-import { Question, QuestionType } from "../api/question";
-import ApiClient from "../api/client";
-
-interface PathParams {
- id: string
-}
-
-interface NavigationProps {
- form_state: boolean, // Whether the form is open or not
- scopes: OAuthScopes[]
-}
-
-class Navigation extends React.Component<NavigationProps> {
- static containerStyles = css`
- margin: auto;
- width: 50%;
-
- text-align: center;
- font-size: 1.5rem;
-
- > div {
- display: inline-block;
- margin: 2rem auto;
- width: 50%;
- }
-
- @media (max-width: 870px) {
- width: 100%;
-
- > div {
- display: flex;
- justify-content: center;
-
- margin: 0 auto;
- }
- }
-
- .return_button {
- text-align: left;
- }
-
- .return_button.closed {
- text-align: center;
- }
- `;
-
- separatorStyles = css`
- height: 0;
- display: none;
-
- @media (max-width: 870px) {
- display: block;
- }
- `;
-
- static returnStyles = css`
- padding: 0.5rem 2.2rem;
- border-radius: 8px;
-
- color: white;
- text-decoration: none;
- white-space: nowrap;
-
- background-color: ${colors.greyple};
- transition: background-color 300ms;
-
- :hover {
- background-color: ${colors.darkerGreyple};
- }
- `;
-
- constructor(props: NavigationProps) {
- super(props);
- this.state = {"logged_in": false};
- }
-
- render(): JSX.Element {
- let submit = null;
-
- if (this.props.form_state) {
- let inner_submit;
- if (this.props.scopes.includes(OAuthScopes.Identify) && !checkScopes(this.props.scopes)) {
- // Render OAuth button if login is required, and the scopes needed are not available
- inner_submit = <OAuth2Button scopes={this.props.scopes} rerender={() => this.setState({"logged_in": true})}/>;
- } else {
- inner_submit = <button form="form" type="submit">Submit</button>;
- }
- submit = <div css={submitStyles}>{ inner_submit }</div>;
- }
-
- return (
- <div css={[unselectable, Navigation.containerStyles]}>
- <div className={ "return_button" + (this.props.form_state ? "" : " closed") }>
- <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
- </div>
- <br css={this.separatorStyles}/>
- { submit }
- </div>
- );
- }
-}
-
-const formStyles = css`
- margin: auto;
- width: 50%;
-
- @media (max-width: 800px) {
- /* Make form larger on mobile and tablet screens */
- width: 80%;
- }
-`;
-
-const closedHeaderStyles = css`
- margin-bottom: 2rem;
- padding: 1rem 4rem;
- border-radius: 8px;
-
- text-align: center;
- font-size: 1.5rem;
-
- background-color: ${colors.error};
-
- @media (max-width: 500px) {
- padding: 1rem 1.5rem;
- }
-`;
-
-function FormPage(): JSX.Element {
- const { id } = useParams<PathParams>();
-
- const [form, setForm] = useState<Form>();
- const [sending, setSending] = useState<boolean>();
- const [sent, setSent] = useState<boolean>();
-
- const bottomDivRef = createRef<HTMLDivElement>();
-
- useEffect(() => {
- getForm(id).then(form => {
- setForm(form);
- });
- }, []);
-
- if (form && sent) {
- const thanksStyle = css`font-family: "Uni Sans", "Hind", "Arial", sans-serif; margin-top: 15.5rem;`;
- const divStyle = css`width: 80%;`;
-
- let submitted_text;
- if (form.submitted_text) {
- submitted_text = form.submitted_text.split("\n").map((line, index) => <span key={index}>{line}<br/></span>);
- submitted_text.push(<span key={submitted_text.length - 1}>{submitted_text.pop()?.props.children[0]}</span>);
- } else {
- submitted_text = "Thanks for your response!";
- }
-
- return (
- <div>
- <HeaderBar title={form.name} description={form.description}/>
- <div css={[unselectable, Navigation.containerStyles, divStyle]}>
- <h3 css={thanksStyle}>{submitted_text}</h3>
- <div className={ "return_button closed" }>
- <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
- </div>
- </div>
- </div>
- );
- }
-
- if (sending) {
- return (
- <div>
- <HeaderBar title={"Submitting..."}/>
- <div css={{display: "flex", justifyContent: "center", paddingTop: "40px"}}>
- <PropagateLoader color="white"/>
- </div>
- </div>
- );
- }
-
- if (!form) {
- return <Loading/>;
- }
-
- const refMap: Map<string, React.RefObject<RenderedQuestion>> = new Map();
- const questions = form.questions.map((question, index) => {
- const questionRef = createRef<RenderedQuestion>();
- refMap.set(question.id, questionRef);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- return <RenderedQuestion ref={questionRef} focus_ref={createRef<any>()} scroll_ref={createRef<HTMLDivElement>()} question={question} public_state={new Map()} key={index + Date.now()}/>;
- });
-
- const open: boolean = form.features.includes(FormFeatures.Open);
- 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); }
- }
-
- let closed_header = null;
- if (!open) {
- closed_header = <div css={closedHeaderStyles}>This form is now closed. You will not be able to submit your response.</div>;
- }
-
- async function handleSubmit(event: SyntheticEvent) {
- event.preventDefault();
- // Client-side required validation
- const invalidFieldIDs: number[] = [];
- questions.forEach((prop, i) => {
- const question: Question = prop.props.question;
- if (!question.required) {
- return;
- }
-
- const questionRef = refMap.get(question.id);
- if (questionRef && questionRef.current) {
- if(questionRef.current.props.question.type == QuestionType.Code){
- questionRef.current.props.public_state.set("unittestsFailed", false);
- }
- questionRef.current.validateField();
- }
- // In case when field is invalid, add this to invalid fields list.
- if (prop.props.public_state.get("valid") === false) {
- invalidFieldIDs.push(i);
- }
- });
-
- if (invalidFieldIDs.length) {
- const firstErrored = questions[invalidFieldIDs[0]];
- if (firstErrored && firstErrored.props.scroll_ref) {
- // If any element is already focused, unfocus it to avoid not scrolling behavior.
- if (document.activeElement && document.activeElement instanceof HTMLElement) {
- document.activeElement.blur();
- }
-
- firstErrored.props.scroll_ref.current.scrollIntoView({ behavior: "smooth", block: "center" });
- if (firstErrored.props.focus_ref && firstErrored.props.focus_ref.current) {
- firstErrored.props.focus_ref.current.focus({ preventScroll: true });
- }
- }
- return;
- }
-
- // Scroll to bottom when OAuth2 required
- if (scopes.length && !checkScopes(scopes)) {
- bottomDivRef.current.scrollIntoView({ behavior: "smooth", block: "end"});
-
- return;
- }
-
-
- const answers: { [key: string]: unknown } = {};
- questions.forEach(prop => {
- const question: Question = prop.props.question;
- const options: string | string[] = question.data["options"];
-
- // Parse input from each question
- switch (question.type) {
- case QuestionType.Section:
- answers[question.id] = false;
- break;
-
- case QuestionType.Checkbox: {
- if (typeof options !== "string") {
- const keys: Map<string, string> = new Map();
- options.forEach((val: string, index) => {
- keys.set(val, `${("000" + index).slice(-4)}. ${val}`);
- });
- const pairs: { [key: string]: boolean } = { };
- keys.forEach((val, key) => {
- pairs[key] = !!prop.props.public_state.get(val);
- });
- answers[question.id] = pairs;
- }
- break;
- }
-
- case QuestionType.Code:
- default:
- answers[question.id] = prop.props.public_state.get("value");
- }
- });
- await ApiClient.post(`forms/submit/${id}`, { response: answers })
- .then(() => {
- setSending(true);
- setSending(false);
- setSent(true);
- })
- .catch((err: AxiosError) => {
- switch (err.response.status) {
- case 422: {
- // Validation on a submitted code questions
- const questionId =
- err.response.data.test_results[0].question_id;
- questions.forEach((prop) => {
- const question: Question = prop.props.question;
- const questionRef = refMap.get(question.id);
- if (question.id === questionId) {
- prop.props.public_state.set("unittestsFailed", true);
- questionRef.current.validateField();
- }
- });
- break;
- }
- default:
- throw err;
- }
- });
- }
- return (
- <div>
- <HeaderBar title={form.name} description={form.description}/>
-
- <div>
- <form id="form" onSubmit={handleSubmit} css={[formStyles, unselectable]}>
- { closed_header }
- { questions }
- </form>
- <Navigation form_state={open} scopes={scopes}/>
- </div>
-
- <div css={css`margin-bottom: 10rem`} ref={bottomDivRef}/>
- <ScrollToTop/>
- </div>
- );
-}
-
-export default FormPage;