diff options
Diffstat (limited to 'src/pages')
-rw-r--r-- | src/pages/FormPage.tsx | 124 |
1 files changed, 110 insertions, 14 deletions
diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx index c49b9fd..1e331b9 100644 --- a/src/pages/FormPage.tsx +++ b/src/pages/FormPage.tsx @@ -2,8 +2,9 @@ import { jsx, css } from "@emotion/react"; import { Link } from "react-router-dom"; -import React, { SyntheticEvent, useEffect, useState } from "react"; +import React, { SyntheticEvent, useEffect, useState, createRef } from "react"; import { useParams } from "react-router"; +import { PropagateLoader } from "react-spinners"; import HeaderBar from "../components/HeaderBar"; import RenderedQuestion from "../components/Question"; @@ -13,7 +14,8 @@ import ScrollToTop from "../components/ScrollToTop"; import { Form, FormFeatures, getForm } from "../api/forms"; import colors from "../colors"; import { unselectable } from "../commonStyles"; - +import { Question, QuestionType } from "../api/question"; +import ApiClient from "../api/client"; interface PathParams { id: string @@ -24,13 +26,12 @@ interface NavigationProps { } class Navigation extends React.Component<NavigationProps> { - containerStyles = css` + static containerStyles = css` margin: auto; width: 50%; text-align: center; font-size: 1.5rem; - white-space: nowrap; > div { display: inline-block; @@ -67,12 +68,13 @@ class Navigation extends React.Component<NavigationProps> { } `; - returnStyles = css` + static returnStyles = css` padding: 0.5rem 2rem; border-radius: 8px; color: white; text-decoration: none; + white-space: nowrap; background-color: ${colors.greyple}; transition: background-color 300ms; @@ -80,11 +82,11 @@ class Navigation extends React.Component<NavigationProps> { :hover { background-color: ${colors.darkerGreyple}; } - } `; submitStyles = css` text-align: right; + white-space: nowrap; button { padding: 0.5rem 4rem; @@ -116,9 +118,9 @@ class Navigation extends React.Component<NavigationProps> { } return ( - <div css={[unselectable, this.containerStyles]}> + <div css={[unselectable, Navigation.containerStyles]}> <div className={ "return_button" + (this.props.form_state ? "" : " closed") }> - <Link to="/" css={this.returnStyles}>Return Home</Link> + <Link to="/" css={Navigation.returnStyles}>Return Home</Link> </div> <br css={this.separatorStyles}/> { submit } @@ -156,6 +158,8 @@ function FormPage(): JSX.Element { const { id } = useParams<PathParams>(); const [form, setForm] = useState<Form>(); + const [sending, setSending] = useState<boolean>(); + const [sent, setSent] = useState<boolean>(); useEffect(() => { getForm(id).then(form => { @@ -163,26 +167,118 @@ function FormPage(): JSX.Element { }); }, []); + if (form && sent) { + const thanksStyle = css`font-family: "Uni Sans", "Hind", "Arial", sans-serif; margin-top: 15.5rem;`; + const divStyle = css`width: 80%;`; + return ( + <div> + <HeaderBar title={form.name} description={form.description}/> + <div css={[unselectable, Navigation.containerStyles, divStyle]}> + <h3 css={thanksStyle}>{form.submitted_text ?? "Thanks for your response!"}</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) => { - return <RenderedQuestion question={question} public_state={new Map()} key={index + Date.now()}/>; + 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()}/>; }); - function handleSubmit(event: SyntheticEvent) { + 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) { + 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; + } + + setSending(true); + + const answers: { [key: string]: unknown } = {}; questions.forEach(prop => { - const question = prop.props.question; + const question: Question = prop.props.question; + const options: string | string[] = question.data["options"]; - // TODO: Parse input from each question, and submit + // 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: - console.log(question.id, prop.props.public_state); + answers[question.id] = prop.props.public_state.get("value"); } }); - event.preventDefault(); + await ApiClient.post(`forms/submit/${id}`, {response: answers}); + setSending(false); + setSent(true); } const open: boolean = form.features.includes(FormFeatures.Open); |