diff options
| author | 2021-01-04 04:00:10 +0300 | |
|---|---|---|
| committer | 2021-01-06 09:35:59 +0300 | |
| commit | 0da45505d7b5bc4d9b1e4aa1e9489f8b1f165725 (patch) | |
| tree | 2729127b39f68b3969ef58309f4f51457c3bc89c /src | |
| parent | Implements 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.tsx | 128 | ||||
| -rw-r--r-- | src/pages/FormPage.tsx | 76 | ||||
| -rw-r--r-- | src/pages/css/FormPage.css | 452 | 
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 */ +} | 
