aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/App.tsx2
-rw-r--r--src/commonStyles.tsx61
-rw-r--r--src/components/OAuth2Button.tsx6
-rw-r--r--src/pages/FormPage/ErrorPage.tsx29
-rw-r--r--src/pages/FormPage/FormPage.tsx41
-rw-r--r--src/pages/FormPage/Navigation.tsx111
-rw-r--r--src/pages/FormPage/SuccessPage.tsx10
-rw-r--r--src/pages/NotFound.tsx26
-rw-r--r--src/tests/components/OAuth2Button.test.tsx4
9 files changed, 162 insertions, 128 deletions
diff --git a/src/App.tsx b/src/App.tsx
index 5430e40..70e0b11 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,6 +10,7 @@ import { PropagateLoader } from "react-spinners";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import globalStyles from "./globalStyles";
+import NotFound from "./pages/NotFound";
const LandingPage = React.lazy(() => import("./pages/LandingPage"));
const FormPage = React.lazy(() => import("./pages/FormPage/FormPage"));
@@ -41,6 +42,7 @@ function Routing(): JSX.Element {
return (
<Routes location={location}>
{renderedRoutes}
+ <Route path="*" element={<NotFound message={"404: This page does not exist"}/>}/>
</Routes>
);
}
diff --git a/src/commonStyles.tsx b/src/commonStyles.tsx
index bfae17e..b4989da 100644
--- a/src/commonStyles.tsx
+++ b/src/commonStyles.tsx
@@ -51,8 +51,7 @@ const textInputs = css`
border-radius: 8px;
`;
-const submitStyles = css`
- text-align: right;
+const actionButtonStyles = css`
white-space: nowrap;
button:disabled {
@@ -61,6 +60,7 @@ const submitStyles = css`
}
button {
+ width: 100%;
cursor: pointer;
border: none;
@@ -91,12 +91,65 @@ const invalidStyles = css`
}
`;
+const mainTextStyles = css`
+ margin: auto;
+ width: 50%;
+
+ text-align: center;
+ font-size: 1.5rem;
+
+ > div {
+ margin: 2rem auto;
+ }
+
+ @media (max-width: 800px) {
+ width: 80%;
+ }
+`;
+
+const navigationStyles = css`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ align-items: center;
+ flex-wrap: wrap;
+
+ column-gap: 20px;
+ row-gap: 20px;
+
+ > * {
+ // Make all elements the same size
+ flex: 0 1 16rem;
+ }
+`;
+
+const returnButtonStyles = css`
+ font-size: 1.5rem;
+ text-align: center;
+
+ color: white;
+ text-decoration: none;
+ background-color: ${colors.greyple};
+
+ padding: 0.5rem 0;
+ border-radius: 8px;
+
+ transition: background-color 300ms;
+
+ :hover {
+ background-color: ${colors.darkerGreyple};
+ }
+`;
+
export {
selectable,
unselectable,
hiddenInput,
multiSelectInput,
textInputs,
- submitStyles,
- invalidStyles
+ actionButtonStyles,
+ invalidStyles,
+ mainTextStyles,
+ returnButtonStyles,
+ navigationStyles,
};
diff --git a/src/components/OAuth2Button.tsx b/src/components/OAuth2Button.tsx
index 885c080..be8d160 100644
--- a/src/components/OAuth2Button.tsx
+++ b/src/components/OAuth2Button.tsx
@@ -11,7 +11,7 @@ import { selectable } from "../commonStyles";
interface OAuth2ButtonProps {
scopes?: OAuthScopes[],
- rerender: () => void
+ rerender?: () => void
}
const iconStyles = css`
@@ -59,7 +59,7 @@ async function login(props: OAuth2ButtonProps, errorDialog: React.RefObject<HTML
throw reason.Error;
});
- if (checkScopes(props.scopes)) {
+ if (checkScopes(props.scopes) && props.rerender) {
props.rerender();
}
}
@@ -71,7 +71,7 @@ function OAuth2Button(props: OAuth2ButtonProps): JSX.Element {
return <span>
<button disabled={disabled} onClick={() => login(props, errorDialog, setDisabled)}>
<FontAwesomeIcon icon={faDiscord} css={iconStyles}/>
- <span css={textStyles}>Discord Login</span>
+ <span css={textStyles}>Login To Submit</span>
</button>
<div css={[errorStyles, selectable]} ref={errorDialog}/>
</span>;
diff --git a/src/pages/FormPage/ErrorPage.tsx b/src/pages/FormPage/ErrorPage.tsx
index da336cf..351170b 100644
--- a/src/pages/FormPage/ErrorPage.tsx
+++ b/src/pages/FormPage/ErrorPage.tsx
@@ -7,9 +7,7 @@ import HeaderBar from "../../components/HeaderBar";
import {Form} from "../../api/forms";
import {clearAuth} from "../../api/auth";
-import {selectable, submitStyles, unselectable} from "../../commonStyles";
-
-import Navigation from "./Navigation";
+import * as styles from "../../commonStyles";
interface ErrorProps {
@@ -28,19 +26,18 @@ export default function ErrorPage(props: ErrorProps): JSX.Element {
return (
<div>
<HeaderBar title={props.form.name} description={props.form.description}/>
- <div css={[unselectable, Navigation.containerStyles]}>
- <h3 css={selectable}>{props.message}</h3>
- <div className={ "return_button" }>
- <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
- </div>
- <br css={Navigation.separatorStyles}/>
- <div css={submitStyles}>
- <button
- type="button" css={refreshStyles}
- onClick={location.reload.bind(window.location)} // TODO: State should probably be saved here
- >
- Refresh
- </button>
+ <div css={[styles.unselectable, styles.mainTextStyles]}>
+ <h3 css={styles.selectable}>{props.message}</h3>
+ <div css={styles.navigationStyles}>
+ <Link css={styles.returnButtonStyles} to="/">Return Home</Link>
+ <div css={styles.actionButtonStyles}>
+ <button
+ type="button" css={refreshStyles}
+ onClick={location.reload.bind(window.location)} // TODO: State should probably be saved here
+ >
+ Refresh
+ </button>
+ </div>
</div>
</div>
</div>
diff --git a/src/pages/FormPage/FormPage.tsx b/src/pages/FormPage/FormPage.tsx
index 9f74410..05b51c0 100644
--- a/src/pages/FormPage/FormPage.tsx
+++ b/src/pages/FormPage/FormPage.tsx
@@ -1,9 +1,11 @@
/** @jsx jsx */
+/** @jsxFrag */
import {jsx, css} from "@emotion/react";
import React, {createRef, SyntheticEvent, useEffect, useState} 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";
@@ -19,6 +21,7 @@ import handleSubmit, {FormState} from "./submit";
import Navigation from "./Navigation";
import Success from "./SuccessPage";
import ErrorPage from "./ErrorPage";
+import NotFound from "../NotFound";
export type RefMapType = Map<string, React.RefObject<RenderedQuestion>>;
@@ -49,24 +52,47 @@ const closedHeaderStyles = css`
}
`;
+enum LoadingState {
+ Pending,
+ Found,
+ Missing
+}
+
function FormPage(): JSX.Element {
const {id} = useParams<{id: string}>();
const [form, setForm] = useState<Form>();
+ const [formLoading, setFormLoading] = useState<LoadingState>(LoadingState.Pending);
const [state, setState] = useState<string>(FormState.INITIAL);
const OAuthRef = createRef<HTMLDivElement>();
useEffect(() => {
- // This can't be null due to the routing to get here
+ // ID can't be null due to the routing to get here
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
getForm(id!).then(form => {
setForm(form);
+ setFormLoading(LoadingState.Found);
+ }).catch((error: AxiosError) => {
+ if (error.response?.status === 404) {
+ setFormLoading(LoadingState.Missing);
+ return;
+ }
+
+ throw error;
});
}, []);
+ switch (formLoading) {
+ case LoadingState.Pending:
+ return <Loading/>;
+ case LoadingState.Missing:
+ return <NotFound message={"Could not find a matching form. Make sure the requested form exists and is open."}/>;
+ }
+
if (!form) {
- return <Loading/>;
+ // This should be an impossible state
+ throw Error("Form was not set despite loading state being set to found.");
}
const refMap: RefMapType = new Map();
@@ -88,9 +114,6 @@ function FormPage(): JSX.Element {
<div css={closedHeaderStyles}>This form is now closed. You will not be able to submit your response.</div>;
}
- // FIXME: Remove this ignore
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
const questions: RenderedQuestion[] = form.questions.map((question, index) => {
const questionRef = createRef<RenderedQuestion>();
refMap.set(question.id, questionRef);
@@ -102,7 +125,7 @@ function FormPage(): JSX.Element {
key={index + Date.now()}
selfRef={questionRef}
ref={questionRef}
- />;
+ /> as unknown as RenderedQuestion; // Annotations for JSX elements resolve to a generic ReactElement
});
switch (state) {
@@ -133,8 +156,10 @@ function FormPage(): JSX.Element {
<div>
<form id="form" onSubmit={handler} css={[formStyles, unselectable]}>
- {closed_header}
- {questions}
+ <>
+ {closed_header}
+ {questions}
+ </>
</form>
<Navigation form_state={open} scopes={scopes}/>
</div>
diff --git a/src/pages/FormPage/Navigation.tsx b/src/pages/FormPage/Navigation.tsx
index 52cd47e..20c7dce 100644
--- a/src/pages/FormPage/Navigation.tsx
+++ b/src/pages/FormPage/Navigation.tsx
@@ -1,12 +1,9 @@
/** @jsx jsx */
-import {jsx, css} from "@emotion/react";
-
-import React from "react";
+import {jsx} from "@emotion/react";
+import React, {useState} from "react";
import {Link} from "react-router-dom";
-import colors from "../../colors";
-import {submitStyles, unselectable} from "../../commonStyles";
-
+import * as styles from "../../commonStyles";
import {checkScopes, OAuthScopes} from "../../api/auth";
import OAuth2Button from "../../components/OAuth2Button";
@@ -16,92 +13,28 @@ interface NavigationProps {
scopes: OAuthScopes[]
}
-export default class Navigation extends React.Component<NavigationProps> {
- 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;
+export default function Navigation(props: NavigationProps): JSX.Element {
+ const [authorized, setAuth] = useState<boolean>(!(
+ props.scopes.includes(OAuthScopes.Identify) && !checkScopes(props.scopes)
+ ));
+
+ let submit = null;
+ if (props.form_state) {
+ let innerElement;
+ if (!authorized) {
+ innerElement = <OAuth2Button rerender={() => setAuth(true)} scopes={props.scopes}/>;
+ } else {
+ innerElement = <button form="form" type="submit">Submit</button>;
}
- }
-
- .return_button {
- text-align: left;
- }
-
- .return_button.closed {
- text-align: center;
- }
- `;
-
- static 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};
+ submit = <div css={styles.actionButtonStyles}>{innerElement}</div>;
}
- 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 = <OAuth2Button scopes={this.props.scopes} rerender={() => this.setState({"logged_in": true})}/>;
- } else {
- inner_submit = <button form="form" type="submit">Submit</button>;
- }
- submit = <div css={submitStyles}>{ inner_submit }</div>;
- }
-
- return (
- <div css={[unselectable, Navigation.containerStyles]}>
- <div className={ "return_button" + (this.props.form_state ? "" : " closed") }>
- <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
- </div>
- <br css={Navigation.separatorStyles}/>
+ return (
+ <div css={[styles.unselectable, styles.mainTextStyles]}>
+ <div css={styles.navigationStyles}>
+ <Link to="/" css={styles.returnButtonStyles}>Return Home</Link>
{ submit }
</div>
- );
- }
+ </div>
+ );
}
diff --git a/src/pages/FormPage/SuccessPage.tsx b/src/pages/FormPage/SuccessPage.tsx
index e35bd4d..e83ca0d 100644
--- a/src/pages/FormPage/SuccessPage.tsx
+++ b/src/pages/FormPage/SuccessPage.tsx
@@ -4,9 +4,7 @@ import {Link} from "react-router-dom";
import {Form} from "../../api/forms";
import HeaderBar from "../../components/HeaderBar";
-import {unselectable} from "../../commonStyles";
-
-import Navigation from "./Navigation";
+import {returnButtonStyles, navigationStyles, unselectable, mainTextStyles} from "../../commonStyles";
interface SuccessProps {
@@ -34,10 +32,10 @@ export default function Success(props: SuccessProps): JSX.Element {
return (
<div>
<HeaderBar title={props.form.name} description={props.form.description}/>
- <div css={[unselectable, Navigation.containerStyles, divStyle]}>
+ <div css={[unselectable, mainTextStyles, divStyle]}>
<h3 css={thanksStyle}>{submitted_text}</h3>
- <div className={"return_button closed"}>
- <Link to="/" css={Navigation.returnStyles}>Return Home</Link>
+ <div css={navigationStyles}>
+ <Link to="/" css={returnButtonStyles}>Return Home</Link>
</div>
</div>
</div>
diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx
new file mode 100644
index 0000000..6947a38
--- /dev/null
+++ b/src/pages/NotFound.tsx
@@ -0,0 +1,26 @@
+/** @jsx jsx */
+import {jsx, css} from "@emotion/react";
+import {Link} from "react-router-dom";
+
+import HeaderBar from "../components/HeaderBar";
+import {mainTextStyles, navigationStyles, returnButtonStyles, unselectable} from "../commonStyles";
+
+interface NotFoundProps {
+ message: string
+}
+
+
+/** Simple 404 page. */
+export default function NotFound(props: NotFoundProps): JSX.Element {
+ return <div>
+ <HeaderBar/>
+ <div css={css`width: 80%; margin: auto;`}>
+ <div css={mainTextStyles}>
+ <p>{props.message}</p>
+ </div>
+ <div css={[unselectable, navigationStyles]}>
+ <Link css={returnButtonStyles} to="/">Return Home</Link>
+ </div>
+ </div>
+ </div>;
+}
diff --git a/src/tests/components/OAuth2Button.test.tsx b/src/tests/components/OAuth2Button.test.tsx
index a773686..2a67f98 100644
--- a/src/tests/components/OAuth2Button.test.tsx
+++ b/src/tests/components/OAuth2Button.test.tsx
@@ -3,8 +3,8 @@ import { render } from "@testing-library/react";
import OAuth2Button from "../../components/OAuth2Button";
test("renders oauth2 sign in button text", () => {
- const { getByText } = render(<OAuth2Button />);
- const button = getByText(/Discord Login/i);
+ const { getByText } = render(<OAuth2Button/>);
+ const button = getByText(/Login To Submit/i);
expect(button).toBeInTheDocument();
});