From ce316baf3c5615122e3ab41ceedbc78137e86b48 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:25:55 +0300 Subject: Updates Models Changes the models to match the backend. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/forms.ts | 9 ++++++++- src/api/question.ts | 2 +- src/tests/components/FormListing.test.tsx | 12 ++++++++---- src/tests/pages/LandingPage.test.tsx | 6 ++++-- 4 files changed, 21 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/api/forms.ts b/src/api/forms.ts index aec4b99..3869838 100644 --- a/src/api/forms.ts +++ b/src/api/forms.ts @@ -6,17 +6,24 @@ export enum FormFeatures { RequiresLogin = "REQUIRES_LOGIN", Open = "OPEN", CollectEmail = "COLLECT_EMAIL", - DisableAntispam = "DISABLE_ANTISPAM" + DisableAntispam = "DISABLE_ANTISPAM", + WEBHOOK_ENABLED = "WEBHOOK_ENABLED" } export interface Form { id: string, features: Array, + webhook: WebHook | null, questions: Array, name: string, description: string } +export interface WebHook { + url: string, + message: string | null +} + export async function getForms(): Promise { const resp = await ApiClient.get("forms/discoverable"); return resp.data; diff --git a/src/api/question.ts b/src/api/question.ts index 18951b9..9824b60 100644 --- a/src/api/question.ts +++ b/src/api/question.ts @@ -13,6 +13,6 @@ export interface Question { id: string, name: string, type: QuestionType, - data: { [key: string]: any }, + data: { [key: string]: string | string[] }, required: boolean } diff --git a/src/tests/components/FormListing.test.tsx b/src/tests/components/FormListing.test.tsx index ad76381..f071c33 100644 --- a/src/tests/components/FormListing.test.tsx +++ b/src/tests/components/FormListing.test.tsx @@ -17,9 +17,11 @@ const openFormListing: Form = { "id": "my-question", "name": "My question", "type": QuestionType.ShortText, - "data": {} + "data": {}, + required: false } - ] + ], + webhook: null }; const closedFormListing: Form = { @@ -32,9 +34,11 @@ const closedFormListing: Form = { "id": "what-should-i-ask", "name": "What should I ask?", "type": QuestionType.ShortText, - "data": {} + "data": {}, + required: false } - ] + ], + webhook: null }; test("renders form listing with specified title", () => { diff --git a/src/tests/pages/LandingPage.test.tsx b/src/tests/pages/LandingPage.test.tsx index 5635e63..e461815 100644 --- a/src/tests/pages/LandingPage.test.tsx +++ b/src/tests/pages/LandingPage.test.tsx @@ -17,9 +17,11 @@ const testingForm: forms.Form = { "id": "my-question", "name": "My Question", "type": QuestionType.ShortText, - "data": {} + "data": {}, + required: true } - ] + ], + "webhook": null }; test("renders landing page", () => { -- cgit v1.2.3 From e6f71e521c437971b376b845b3bc6bac40b8396a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:27:36 +0300 Subject: Implements Form Fetching Fetches forms from the backend to be displayed. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/api/forms.ts | 29 +++++++---------------------- 1 file changed, 7 insertions(+), 22 deletions(-) (limited to 'src') diff --git a/src/api/forms.ts b/src/api/forms.ts index 3869838..8c31e5b 100644 --- a/src/api/forms.ts +++ b/src/api/forms.ts @@ -1,4 +1,4 @@ -import { Question, QuestionType } from "./question"; +import { Question } from "./question"; import ApiClient from "./client"; export enum FormFeatures { @@ -25,26 +25,11 @@ export interface WebHook { } export async function getForms(): Promise { - const resp = await ApiClient.get("forms/discoverable"); - return resp.data; + const fetch_response = await ApiClient.get("forms/discoverable"); + return fetch_response.data; } -export function getForm(id: string): Promise
{ - const data: Form = { - name: "Ban Appeals", - id: "ban-appeals", - description: "Appealing bans from the Discord server", - features: [FormFeatures.Discoverable, FormFeatures.Open], - questions: [ - { - id: "how-spanish-are-you", - name: "How Spanish are you?", - type: QuestionType.ShortText, - data: {} - } - ] - }; - return new Promise((resolve) => { - setTimeout(() => resolve(data), 1500); - }); -} +export async function getForm(id: string): Promise { + const fetch_response = await ApiClient.get(`forms/${id}`); + return fetch_response.data; +} -- cgit v1.2.3 From d2cc29d22203cfea0adc61ceaa72ba936070045a Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:29:27 +0300 Subject: Updates HeaderBar Changes header bar component to accept a description, and render it properly on different screens. Additionally adds a button to return to the home page. Updates tests. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/HeaderBar/index.tsx | 131 ++++++++++++++++++++++---------- src/components/HeaderBar/logo.svg | 3 + src/tests/components/HeaderBar.test.tsx | 23 +++++- 3 files changed, 115 insertions(+), 42 deletions(-) create mode 100644 src/components/HeaderBar/logo.svg (limited to 'src') diff --git a/src/components/HeaderBar/index.tsx b/src/components/HeaderBar/index.tsx index dfe3957..439851d 100644 --- a/src/components/HeaderBar/index.tsx +++ b/src/components/HeaderBar/index.tsx @@ -1,59 +1,110 @@ /** @jsx jsx */ -import { css, jsx } from "@emotion/react"; +import { jsx, css } from "@emotion/react"; import Header1 from "./header_1.svg"; import Header2 from "./header_2.svg"; +import Logo from "./logo.svg"; + +import { Link } from "react-router-dom"; interface HeaderBarProps { - title?: string + title?: string + description?: string } const headerImageStyles = css` -z-index: -1; -top: 0; -position: absolute; -width: 100%; -transition: height 1s; + * { + z-index: -1; + top: 0; + position: absolute; + width: 100%; + transition: height 1s; + } +`; + +const headerTextStyles = css` + transition: margin 1s; + font-family: "Uni Sans", "Hind", "Arial", sans-serif; + + margin: 0 2rem 10rem 2rem; + + .title { + font-size: 3vmax; + margin-bottom: 0; + } + + .description { + font-size: 1.5vmax; + } + + .title, .description { + transition: font-size 1s; + } + + @media (max-width: 480px) { + margin-top: 7rem; + text-align: center; + + .title { + font-size: 5vmax; + } + + .description { + font-size: 2vmax; + } + } +`; + +const homeButtonStyles = css` + svg { + transform: scale(0.25); + transition: top 300ms, transform 300ms; + + @media (max-width: 480px) { + transform: scale(0.15); + } + } + + * { + position: absolute; + top: -10rem; + right: 1rem; + + z-index: 0; + transform-origin: right; + + @media (max-width: 700px) { + top: -11.5rem; + } + + @media (max-width: 480px) { + top: -12.5rem; + } + } `; -function HeaderBar({ title }: HeaderBarProps): JSX.Element { +function HeaderBar({ title, description }: HeaderBarProps): JSX.Element { if (!title) { title = "Python Discord Forms"; } - - return
+ + return (
- - +
+ + +
+ +
+

{title}

+

{description}

+
+ + + +
-

- {title} -

-
; + ); } export default HeaderBar; diff --git a/src/components/HeaderBar/logo.svg b/src/components/HeaderBar/logo.svg new file mode 100644 index 0000000..e7f43fc --- /dev/null +++ b/src/components/HeaderBar/logo.svg @@ -0,0 +1,3 @@ + +image/svg+xml + diff --git a/src/tests/components/HeaderBar.test.tsx b/src/tests/components/HeaderBar.test.tsx index 9c232ad..dd77c8b 100644 --- a/src/tests/components/HeaderBar.test.tsx +++ b/src/tests/components/HeaderBar.test.tsx @@ -2,16 +2,35 @@ import React from "react"; import { render } from "@testing-library/react"; import "@testing-library/jest-dom/extend-expect"; import HeaderBar from "../../components/HeaderBar"; +import { MemoryRouter } from "react-router-dom"; test("renders header bar with title", () => { - const { getByText } = render(); + const { getByText } = render(); const formListing = getByText(/Python Discord Forms/i); expect(formListing).toBeInTheDocument(); }); test("renders header bar with custom title", () => { - const { getByText } = render(); + const { getByText } = render(); const formListing = getByText(/Testing title/i); expect(formListing).toBeInTheDocument(); }); +test("renders header bar with custom description", () => { + const { getByText } = render(); + const formListing = getByText(/Testing description/i); + expect(formListing).toBeInTheDocument(); +}); + +test("renders header bar with custom title and description", () => { + const { getByText } = render( + + + + ); + + const title = getByText(/Testing title/i); + const description = getByText(/Testing description/i); + expect(title).toBeInTheDocument(); + expect(description).toBeInTheDocument(); +}); -- cgit v1.2.3 From c8046900fd94baa5264e15a527c24346be024b73 Mon Sep 17 00:00:00 2001 From: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> Date: Mon, 4 Jan 2021 02:33:35 +0300 Subject: Implements Scroll Button Adds a scroll to top button to the landing page, and form pages to make navigation easier on long pages. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/ScrollToTop.tsx | 87 ++++++++++++++++++++++++++++++++++++++++++ src/pages/LandingPage.tsx | 7 ++-- 2 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 src/components/ScrollToTop.tsx (limited to 'src') diff --git a/src/components/ScrollToTop.tsx b/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..6af938d --- /dev/null +++ b/src/components/ScrollToTop.tsx @@ -0,0 +1,87 @@ +/** @jsx jsx */ +import { jsx, css } from "@emotion/react"; +import React from "react"; + +const styles = css` + width: 2.5rem; + height: 2.5rem; + + position: fixed; + bottom: 3rem; + right: 3rem; + + background-color: #7289DA; /* Blurple */ + border-radius: 50%; + + opacity: 0; + transition: opacity 300ms; + + :after { + display: inline-block; + content: ""; + + position: fixed; + bottom: 3.5rem; + right: 3.65rem; + + border: solid whitesmoke; + border-width: 0.35rem 0.35rem 0 0; + padding: 0.4rem; + + transform: rotate(-45deg); + } + + @media (max-width: 800px) { + bottom: 1.5rem; + right: 1.5rem; + + :after { + bottom: 2rem; + right: 2.15rem; + } + } +`; + +let last_ref: React.RefObject; + +class ScrollToTop extends React.Component { + constructor(props: Record) { + super(props); + last_ref = React.createRef(); + } + + handleScroll(): void { + if (!last_ref.current) return; + + if (window.pageYOffset > 250) { + last_ref.current.style.opacity = "1"; + } else { + last_ref.current.style.opacity = "0"; + } + } + + componentDidMount(): void { + // Register event handler + window.addEventListener("scroll", this.handleScroll, {passive: true}); + } + + componentDidUpdate(): void { + // Hide previous iterations, and register handler for current one + if (last_ref.current) { + last_ref.current.style.opacity = "0"; + } + + window.addEventListener("scroll", this.handleScroll, {passive: true}); + } + + componentWillUnmount(): void { + // Unregister handler + window.removeEventListener("scroll", this.handleScroll); + } + + render(): JSX.Element { + return
window.scrollTo({top: 0, behavior: "smooth"})}/>; + } +} + +export default ScrollToTop; diff --git a/src/pages/LandingPage.tsx b/src/pages/LandingPage.tsx index 124bbcf..af968ad 100644 --- a/src/pages/LandingPage.tsx +++ b/src/pages/LandingPage.tsx @@ -4,10 +4,11 @@ import { useEffect, useState } from "react"; import HeaderBar from "../components/HeaderBar"; import FormListing from "../components/FormListing"; - -import { getForms, Form } from "../api/forms"; import OAuth2Button from "../components/OAuth2Button"; import Loading from "../components/Loading"; +import ScrollToTop from "../components/ScrollToTop"; + +import { getForms, Form } from "../api/forms"; function LandingPage(): JSX.Element { const [forms, setForms] = useState(); @@ -25,8 +26,8 @@ function LandingPage(): JSX.Element { return
+
-
Date: Mon, 4 Jan 2021 03:57:10 +0300 Subject: Implements Input Types Adds functionality and JSX for all input types. Adds a dispatcher that can pick and return the needed element. Signed-off-by: Hassan Abouelela <47495861+HassanAbouelela@users.noreply.github.com> --- src/components/InputTypes/Checkbox.tsx | 22 ++++++++++ src/components/InputTypes/Code.tsx | 11 +++++ src/components/InputTypes/Radio.tsx | 18 +++++++++ src/components/InputTypes/Range.tsx | 52 ++++++++++++++++++++++++ src/components/InputTypes/Select.tsx | 60 ++++++++++++++++++++++++++++ src/components/InputTypes/ShortText.tsx | 11 +++++ src/components/InputTypes/TextArea.tsx | 11 +++++ src/components/InputTypes/index.tsx | 71 +++++++++++++++++++++++++++++++++ 8 files changed, 256 insertions(+) create mode 100644 src/components/InputTypes/Checkbox.tsx create mode 100644 src/components/InputTypes/Code.tsx create mode 100644 src/components/InputTypes/Radio.tsx create mode 100644 src/components/InputTypes/Range.tsx create mode 100644 src/components/InputTypes/Select.tsx create mode 100644 src/components/InputTypes/ShortText.tsx create mode 100644 src/components/InputTypes/TextArea.tsx create mode 100644 src/components/InputTypes/index.tsx (limited to 'src') diff --git a/src/components/InputTypes/Checkbox.tsx b/src/components/InputTypes/Checkbox.tsx new file mode 100644 index 0000000..ed02b83 --- /dev/null +++ b/src/components/InputTypes/Checkbox.tsx @@ -0,0 +1,22 @@ +/** @jsx jsx */ +import { jsx } from "@emotion/react"; +import React, { ChangeEvent } from "react"; + +interface CheckboxProps { + index: number, + option: string, + handler: (event: ChangeEvent) => void +} + +export default function Checkbox(props: CheckboxProps): JSX.Element { + return ( + + ); +} diff --git a/src/components/InputTypes/Code.tsx b/src/components/InputTypes/Code.tsx new file mode 100644 index 0000000..51ca98d --- /dev/null +++ b/src/components/InputTypes/Code.tsx @@ -0,0 +1,11 @@ +/** @jsx jsx */ +import { jsx } from "@emotion/react"; +import React, { ChangeEvent } from "react"; + +interface CodeProps { + handler: (event: ChangeEvent) => void +} + +export default function Code(props: CodeProps): JSX.Element { + return ; +} diff --git a/src/components/InputTypes/Radio.tsx b/src/components/InputTypes/Radio.tsx new file mode 100644 index 0000000..81f8375 --- /dev/null +++ b/src/components/InputTypes/Radio.tsx @@ -0,0 +1,18 @@ +/** @jsx jsx */ +import { jsx } from "@emotion/react"; +import React, { ChangeEvent } from "react"; + +interface RadioProps { + option: string, + question_id: string, + handler: (event: ChangeEvent) => void +} + +export default function Radio(props: RadioProps): JSX.Element { + return ( + + ); +} diff --git a/src/components/InputTypes/Range.tsx b/src/components/InputTypes/Range.tsx new file mode 100644 index 0000000..a0260ad --- /dev/null +++ b/src/components/InputTypes/Range.tsx @@ -0,0 +1,52 @@ +/** @jsx jsx */ +import { jsx } from "@emotion/react"; +import React from "react"; + +interface RangeProps { + question_id: string, + options: Array, + state_dict: Map +} + +let last_selection: Element; + +interface handler_props { + state_dict: Map, + ref: React.RefObject +} + +function handler(this: handler_props): void { + if (last_selection) { + last_selection.classList.toggle("selected"); + } + + const dot: Element = this.ref.current!.lastElementChild!; // eslint-disable-line + dot.classList.toggle("selected"); + + last_selection = dot; + + const value: string = this.ref.current!.textContent!; //eslint-disable-line + this.state_dict.set("value", value); +} + +export default function Range(props: RangeProps): JSX.Element { + const range = props.options.map((option, index) => { + const ref: React.RefObject = React.createRef(); + return ( +