aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorGravatar Hassan Abouelela <[email protected]>2021-01-04 04:00:10 +0300
committerGravatar Hassan Abouelela <[email protected]>2021-01-06 09:35:59 +0300
commit0da45505d7b5bc4d9b1e4aa1e9489f8b1f165725 (patch)
tree2729127b39f68b3969ef58309f4f51457c3bc89c /src
parentImplements Input Types (diff)
Adds Question Rendering
Adds a question component, and calls it on form page. Adds styling for input types and form page. Lays foundation for validation and submission. Signed-off-by: Hassan Abouelela <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/components/Question.tsx128
-rw-r--r--src/pages/FormPage.tsx76
-rw-r--r--src/pages/css/FormPage.css452
3 files changed, 646 insertions, 10 deletions
diff --git a/src/components/Question.tsx b/src/components/Question.tsx
new file mode 100644
index 0000000..1c0fb31
--- /dev/null
+++ b/src/components/Question.tsx
@@ -0,0 +1,128 @@
+/** @jsx jsx */
+import { jsx } from "@emotion/react";
+import React, { ChangeEvent } from "react";
+
+import { Question, QuestionType } from "../api/question";
+import create_input from "./InputTypes";
+
+const _skip_normal_state: Array<QuestionType> = [
+ QuestionType.Radio,
+ QuestionType.Checkbox,
+ QuestionType.Select,
+ QuestionType.Section,
+ QuestionType.Range
+];
+
+export type QuestionProp = {
+ question: Question,
+ public_state: Map<string, string | boolean | null>,
+}
+
+class RenderedQuestion extends React.Component<QuestionProp> {
+ constructor(props: QuestionProp) {
+ super(props);
+ if (props.question.type === QuestionType.TextArea) {
+ this.handler = this.text_area_handler.bind(this);
+ } else {
+ this.handler = this.normal_handler.bind(this);
+ }
+
+ if (!_skip_normal_state.includes(props.question.type)) {
+ this._setState("value", "");
+ }
+ }
+
+ _setState(target: string, value: string | boolean | null, callback?:() => void): void {
+ this.setState({[target]: value}, callback);
+ this.props.public_state.set(target, value);
+ }
+
+ handler(_: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void {} // eslint-disable-line
+
+ normal_handler(event: ChangeEvent<HTMLInputElement>): void {
+ let target: string;
+ let value: string | boolean;
+
+ switch (event.target.type) {
+ case QuestionType.Checkbox:
+ target = this.props.question.id;
+ value = event.target.checked;
+ break;
+
+ case QuestionType.Radio:
+ target = "value";
+ if (event.target.parentElement) {
+ value = event.target.parentElement.innerText.trimEnd();
+ } else {
+ value = event.target.value;
+ }
+ break;
+
+ case QuestionType.Select:
+ // Handled by component
+ return;
+
+ default:
+ target = "value";
+ value = event.target.value;
+ }
+
+ this._setState(target, value);
+
+ // Toggle checkbox class
+ if (event.target.type == "checkbox" && event.target.parentElement !== null) {
+ event.target.parentElement.classList.toggle("unselected_checkbox_label");
+ event.target.parentElement.classList.toggle("selected_checkbox_label");
+ }
+ }
+
+ text_area_handler(event: ChangeEvent<HTMLTextAreaElement>): void {
+ this._setState("value", event.target.value);
+ }
+
+ componentDidMount(): void {
+ // Initialize defaults for complex and nested fields
+ const options: string | string[] = this.props.question.data["options"];
+
+ if (this.props.public_state.size === 0) {
+ switch (this.props.question.type) {
+ case QuestionType.Checkbox:
+ if (typeof options === "string") {
+ return;
+ }
+
+ options.forEach((option, index) => {
+ this._setState(`${("000" + index).slice(-4)}. ${option}`, false);
+ });
+ break;
+
+ case QuestionType.Range:
+ case QuestionType.Radio:
+ case QuestionType.Select:
+ this._setState("value", null);
+ break;
+ }
+ }
+ }
+
+ render(): JSX.Element {
+ const question = this.props.question;
+
+ if (question.type === QuestionType.Section) {
+ return <div>
+ <h1 className="selectable">{question.name}</h1>
+ { question.data["text"] ? <h3 className="selectable">{question.data["text"]}</h3> : "" }
+ <hr className="section_header"/>
+ </div>;
+ } else {
+ return <div>
+ <h2 className="selectable">
+ {question.name}<span id={question.required ? "required" : ""} className="required_star">*</span>
+ </h2>
+ { create_input(this.props, this.handler) }<hr/>
+ </div>;
+ }
+ }
+}
+
+export default RenderedQuestion;
diff --git a/src/pages/FormPage.tsx b/src/pages/FormPage.tsx
index 1805897..b966e84 100644
--- a/src/pages/FormPage.tsx
+++ b/src/pages/FormPage.tsx
@@ -1,12 +1,17 @@
/** @jsx jsx */
-import { jsx } from "@emotion/react";
+import { jsx, css } from "@emotion/react";
import { Link } from "react-router-dom";
+import React, { SyntheticEvent, useEffect, useState } from "react";
import { useParams } from "react-router";
+
import HeaderBar from "../components/HeaderBar";
-import { useEffect, useState } from "react";
-import { Form, getForm } from "../api/forms";
+import RenderedQuestion from "../components/Question";
import Loading from "../components/Loading";
+import ScrollToTop from "../components/ScrollToTop";
+
+import { Form, FormFeatures, getForm } from "../api/forms";
+
interface PathParams {
id: string
@@ -21,19 +26,70 @@ function FormPage(): JSX.Element {
getForm(id).then(form => {
setForm(form);
});
- });
+ }, []);
if (!form) {
return <Loading/>;
}
- return <div>
- <HeaderBar title={form.name}/>
- <div css={{marginLeft: "20px"}}>
- <h1>{form.description}</h1>
- <Link to="/" css={{color: "white"}}>Return home</Link>
+ const questions = form.questions.map((question, index) => {
+ return <RenderedQuestion question={question} public_state={new Map()} key={index}/>;
+ });
+
+ function handleSubmit(event: SyntheticEvent) {
+ questions.forEach(prop => {
+ const question = prop.props.question;
+
+ // TODO: Parse input from each question, and submit
+ switch (question.type) {
+ default:
+ console.log(question.id, prop.props.public_state);
+ }
+ });
+
+ event.preventDefault();
+ }
+
+ const open: boolean = form.features.includes(FormFeatures.Open);
+
+ let closed_header = null;
+ let submit = null;
+
+ if (open) {
+ submit = (
+ <div className="submit_form">
+ <button form="form" type="submit">Submit</button>
+ </div>
+ );
+ } else {
+ closed_header = (
+ <div className="closed_header">
+ <div>This form is now closed. You will not be able to submit your response.</div>
+ </div>
+ );
+ }
+
+
+ return (
+ <div>
+ <HeaderBar title={form.name} description={form.description} key={2}/>
+ <div css={css`${require("./css/FormPage.css")};`}>
+ <form id="form" onSubmit={handleSubmit} className="unselectable">
+ { closed_header }
+ {questions}
+ </form>
+ <div className="nav unselectable">
+ <div className={ "nav_buttons" + (open ? "" : " closed") }>
+ <Link to="/" className="return_home">Return Home</Link>
+ </div>
+ <br className="nav_separator"/>
+ { submit }
+ </div>
+ </div>
+ <div css={css`margin-bottom: 10rem`}/>
+ <ScrollToTop/>
</div>
- </div>;
+ );
}
export default FormPage;
diff --git a/src/pages/css/FormPage.css b/src/pages/css/FormPage.css
new file mode 100644
index 0000000..254ddef
--- /dev/null
+++ b/src/pages/css/FormPage.css
@@ -0,0 +1,452 @@
+form {
+ margin: auto;
+ width: 50%;
+}
+
+@media (max-width: 800px) {
+ /* Make form larger on mobile and tablet screens */
+ form {
+ width: 80%;
+ }
+}
+
+hr {
+ color: gray;
+ margin: 3rem 0;
+}
+
+h1 {
+ font-size: 2.5rem;
+ margin-bottom: 0;
+ text-align: center;
+}
+
+h3 {
+ margin-top: 0;
+ text-align: center;
+}
+
+.section_header {
+ margin-top: 1rem;
+}
+
+.closed_header {
+ margin-bottom: 2rem;
+ text-align: center;
+}
+
+.closed_header div {
+ font-size: 1.5rem;
+ background-color: #f04747;
+
+ padding: 1rem 4rem;
+ border-radius: 8px;
+}
+
+.unselectable {
+ -moz-user-select: none;
+ -webkit-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+}
+
+.selectable {
+ -moz-user-select: text;
+ -webkit-user-select: text;
+ -ms-user-select: text;
+ user-select: text;
+}
+
+/* ------------------------------------------------------------- */
+/* Required */
+/* ------------------------------------------------------------- */
+.required_star {
+ display: none;
+}
+
+#required.required_star {
+ display: inline-block;
+ position: relative;
+
+ color: red;
+
+ top: -0.2rem;
+ margin-left: 0.2rem;
+}
+
+/* ------------------------------------------------------------- */
+/* Checkboxes */
+/* ------------------------------------------------------------- */
+.checkbox_label {
+ display: inline-block;
+ position: relative;
+ top: 0.25em;
+
+ width: 1em;
+ height: 1em;
+
+ margin: 1rem 0.5rem 0 0;
+ border: whitesmoke 0.2rem solid;
+ border-radius: 25%;
+
+ -webkit-transition: background-color 300ms ease-in-out;
+ transition: background-color 300ms ease-in-out;
+}
+
+.checkbox_label input {
+ position: absolute;
+ opacity: 0;
+ height: 0;
+ width: 0;
+}
+
+.unselected_checkbox_label {
+ background-color: white;
+}
+
+.unselected_checkbox_label:hover {
+ background-color: lightgray;
+}
+
+.selected_checkbox_label {
+ background-color: #7289DA; /* Blurple */
+}
+
+.checkmark_span {
+ position: absolute;
+}
+
+.selected_checkbox_label .checkmark_span {
+ width: 0.30rem;
+ height: 0.60rem;
+ left: 0.25em;
+
+ border: solid white;
+ border-width: 0 0.2rem 0.2rem 0;
+
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+}
+
+/* ------------------------------------------------------------- */
+/* Radio */
+/* ------------------------------------------------------------- */
+input[type="radio"] {
+ margin: 1rem 0.5rem 0 0;
+}
+
+/* ------------------------------------------------------------- */
+/* Select */
+/* ------------------------------------------------------------- */
+.select_container {
+ display: inline-block;
+ position: relative;
+
+ width: min(20rem, 90%);
+ height: 100%;
+ min-height: 2rem;
+
+ background: whitesmoke;
+
+ color: black;
+ text-align: center;
+
+ margin-bottom: 0;
+
+ border: 0.1rem solid black;
+ border-radius: 8px;
+
+ -webkit-transition: border-radius 400ms;
+ transition: border-radius 400ms;
+}
+
+.select_container.active {
+ height: auto;
+ border-radius: 8px 8px 0 0;
+}
+
+.select_arrow {
+ display: inline-block;
+ height: 0.5rem;
+ width: 0.5rem;
+
+ position: relative;
+ float: right;
+ right: 1em;
+ top: 0.7rem;
+
+ border: solid black;
+ border-width: 0 0.2rem 0.2rem 0;
+
+ -webkit-transform: rotate(45deg);
+ -ms-transform: rotate(45deg);
+ transform: rotate(45deg);
+
+ -webkit-transition: transform 400ms;
+ transition: transform 400ms;
+}
+
+.select_container.active .select_arrow {
+ -webkit-transform: translateY(40%) rotate(225deg);
+ -ms-transform: translateY(40%) rotate(225deg);
+ transform: translateY(40%) rotate(225deg);
+}
+
+.selected_option {
+ display: block;
+ padding: 0.5rem 0;
+}
+
+.select_options_container {
+ position: relative;
+ width: 100%;
+
+ /* Need to account for margin */
+ left: -0.1rem;
+}
+
+.select_options {
+ display: block;
+ position: absolute;
+ width: 100%;
+
+ visibility: hidden;
+ opacity: 0;
+
+ background: whitesmoke;
+ overflow: hidden;
+
+ border: 0.1rem solid black;
+ border-radius: 0 0 8px 8px;
+ border-top: none;
+
+ -webkit-transition: opacity 400ms, visibility 400ms;
+ transition: opacity 400ms, visibility 400ms;
+}
+
+.select_container.active .select_options {
+ visibility: visible;
+ opacity: 1;
+}
+
+.select_options > div > div {
+ padding: 0.75rem;
+}
+
+.select_options > div:hover {
+ background-color: lightgray;
+}
+
+.select_options hr {
+ margin: 0 1rem;
+}
+
+/* ------------------------------------------------------------- */
+/* Text Types */
+/* ------------------------------------------------------------- */
+.short_text, .text_area {
+ display: inline-block;
+ width: min(20rem, 90%);
+ height: 100%;
+ min-height: 2rem;
+
+ background: whitesmoke;
+
+ color: black;
+ padding: 0 1rem;
+ font: inherit;
+
+ margin-bottom: 0;
+
+ border: 0.1rem solid black;
+ border-radius: 8px;
+}
+
+.text_area {
+ min-height: 20rem;
+ min-width: 40%;
+ width: 100%;
+ box-sizing: border-box;
+
+ padding: 1rem;
+}
+
+/* ------------------------------------------------------------- */
+/* Range */
+/* ------------------------------------------------------------- */
+.range {
+ display: flex;
+ justify-content: space-between;
+ position: relative;
+ width: 100%;
+}
+
+.range label {
+ width: 1rem;
+}
+
+.range label span {
+ display: inline-block;
+ transform: translateX(-50%);
+ margin: 0 50%;
+
+ white-space: nowrap;
+
+ transition: transform 300ms;
+}
+
+.range_dot {
+ width: 0.8rem;
+ height: 0.8rem;
+ background-color: whitesmoke;
+
+ border: 0.2rem solid whitesmoke;
+ border-radius: 50%;
+
+ transition: background-color 300ms;
+}
+
+.range_dot.selected {
+ background-color: #7289DA; /* Blurple */
+}
+
+.range_slider_container {
+ display: flex;
+ justify-content: center;
+ width: 100%;
+
+ position: absolute;
+ z-index: -1;
+
+ top: 2rem;
+
+ transition: all 300ms;
+}
+
+.range_slider {
+ width: 98%; /* Needs to be slightly smaller than container to work on all devices */
+ height: 0.5rem;
+ background-color: whitesmoke;
+
+ transition: transform 300ms;
+}
+
+/* ------------------------------------------------------------- */
+/* Mobile Range */
+/* ------------------------------------------------------------- */
+@media (max-width: 800px){
+ .range {
+ width: 20%;
+ display: block;
+ margin: 0 auto;
+ }
+
+ .range_dot {
+ margin-bottom: 1.5rem;
+ }
+
+ .range label span {
+ margin-left: 0;
+ transform: translateY(1.6rem) translateX(2rem);
+ }
+
+ .range_slider_container {
+ width: 0.5rem;
+ left: 0.32rem;
+ height: 88%;
+
+ background: whitesmoke;
+ z-index: -1;
+ }
+
+ .range_slider {
+ display: none;
+ }
+}
+
+/* ------------------------------------------------------------- */
+/* Navigation */
+/* ------------------------------------------------------------- */
+.nav {
+ margin: auto;
+ width: 50%;
+
+ text-align: center;
+ font-size: 1.5rem;
+ white-space: nowrap;
+}
+
+.nav_separator {
+ height: 0;
+ display: none;
+}
+
+.nav > div {
+ display: inline-block;
+ margin: 2rem auto;
+ width: 50%;
+}
+
+.nav_buttons {
+ text-align: left;
+}
+
+.nav_buttons.closed {
+ text-align: center;
+}
+
+.submit_form {
+ text-align: right;
+}
+
+/* Tile Buttons Vertically On Smaller Devices */
+@media (max-width: 850px) {
+ .nav {
+ width: 100%;
+ }
+
+ .nav_separator {
+ display: block;
+ }
+
+ .nav > div {
+ display: flex;
+ justify-content: center;
+
+ margin: 0 auto;
+ }
+}
+
+.return_home {
+ padding: 0.5rem 2rem;
+ border-radius: 8px;
+
+ color: white;
+ text-decoration: none;
+
+ background-color: #99AAB5; /* Gray-ish */
+ transition: background-color 300ms;
+}
+
+.return_home:hover {
+ background-color: #6E7D88; /* Darker gray-ish */
+}
+
+.submit_form button {
+ padding: 0.5rem 4rem;
+ cursor: pointer;
+
+ border: none;
+ border-radius: 8px;
+
+ color: white;
+ font: inherit;
+
+ background-color: #7289DA; /* Blurple */
+ transition: background-color 300ms;
+}
+
+.submit_form button:hover {
+ background-color: #4E609C; /* Darker blurple */
+}