/** @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 { 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 = this.setState({"logged_in": true})}/>; } else { inner_submit = ; } submit =
{ inner_submit }
; } return (
Return Home

{ submit }
); } } 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(); const [form, setForm] = useState
(); const [sending, setSending] = useState(); const [sent, setSent] = useState(); const bottomDivRef = createRef(); 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) => {line}
); submitted_text.push({submitted_text.pop()?.props.children[0]}); } else { submitted_text = "Thanks for your response!"; } return (

{submitted_text}

Return Home
); } if (sending) { return (
); } if (!form) { return ; } const refMap: Map> = new Map(); const questions = form.questions.map((question, index) => { const questionRef = createRef(); refMap.set(question.id, questionRef); // eslint-disable-next-line @typescript-eslint/no-explicit-any return ()} scroll_ref={createRef()} 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 =
This form is now closed. You will not be able to submit your response.
; } 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 = 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 (
{ closed_header } { questions }
); } export default FormPage;