window.scrollTo({top: 0, behavior: "smooth"})}/>;
90 | }
91 | }
92 |
93 | export default ScrollToTop;
94 |
--------------------------------------------------------------------------------
/src/components/Tag.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { css, jsx } from "@emotion/react";
3 |
4 | interface TagProps {
5 | text: string,
6 | color: string,
7 | fontSize?: string
8 | }
9 |
10 | function Tag(props: TagProps): JSX.Element {
11 | return
21 | {props.text}
22 | ;
23 | }
24 |
25 | export default Tag;
26 |
--------------------------------------------------------------------------------
/src/globalStyles.ts:
--------------------------------------------------------------------------------
1 | import { css } from "@emotion/react";
2 |
3 | import colors from "./colors";
4 |
5 | export default css`
6 | @import url('https://fonts.googleapis.com/css2?family=Hind:wght@700&display=swap');
7 |
8 | @font-face {
9 | font-family: 'Uni Sans';
10 | src: url(/fonts/unisans.otf) format('opentype');
11 | }
12 |
13 | body {
14 | background-color: ${colors.notQuiteBlack};
15 | color: white;
16 | font-family: "Hind", "Helvetica", "Arial", sans-serif;
17 | margin: 0;
18 | }
19 |
20 | .fade-enter,
21 | .fade-exit {
22 | position: absolute;
23 | top: 0;
24 | left: 0;
25 | transition: 300ms ease-in-out opacity, 300ms ease-in-out transform;
26 | width: 100%;
27 | }
28 |
29 | .fade-enter,
30 | .fade-exit-active {
31 | opacity: 0;
32 | transform: scale(0.98);
33 | }
34 |
35 | .fade-enter-active {
36 | opacity: 1;
37 | z-index: 1;
38 | transform: scale(1);
39 | }
40 | `;
41 |
--------------------------------------------------------------------------------
/src/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "@emotion/react";
3 | import React, { useEffect } from "react";
4 | import { createRoot } from "react-dom/client";
5 | import App from "./App";
6 | import * as serviceWorker from "./serviceWorker";
7 |
8 | import formsStore from "./store";
9 | import { Provider } from "react-redux";
10 |
11 | import * as Sentry from "@sentry/react";
12 |
13 | import {
14 | createRoutesFromChildren,
15 | matchRoutes,
16 | useLocation,
17 | useNavigationType,
18 | } from "react-router-dom";
19 |
20 |
21 | import colors from "./colors";
22 |
23 | if (process.env.NODE_ENV === "production") {
24 | Sentry.init({
25 | dsn: process.env.REACT_APP_SENTRY_DSN,
26 | tracesSampleRate: 0.5,
27 | release: `forms-frontend@${process.env.COMMIT_REF}`,
28 | replaysSessionSampleRate: 0.1,
29 | replaysOnErrorSampleRate: 1.0,
30 | environment: process.env.CONTEXT,
31 | integrations: [
32 | Sentry.reactRouterV6BrowserTracingIntegration({
33 | useEffect,
34 | useLocation,
35 | useNavigationType,
36 | createRoutesFromChildren,
37 | matchRoutes,
38 | }),
39 | Sentry.replayIntegration(),
40 | ]
41 | });
42 |
43 | // Set tag as PR number, "main", or if unavailable, "unknown"
44 | const branch = process.env.BRANCH ?? "unknown";
45 | const branch_name = branch.replace(RegExp("pull/|/head", "g"), "");
46 | Sentry.setTag(branch_name === "main" ? "branch" : "pull_request", branch_name);
47 | }
48 |
49 | console.log("%c Python Discord Forms ", `font-size: 4em; font-family: "Hind", "Arial"; font-weight: 900; background-color: ${colors.blurple}; border-radius: 10px;`);
50 | console.log("%cWelcome to Python Discord Forms", "font-size: 3em; font-family: \"Hind\", \"Arial\";");
51 |
52 | console.log(` Environment: %c ${process.env.NODE_ENV} `, `padding: 2px; border-radius: 5px; background-color: ${process.env.NODE_ENV === "production" ? colors.success : colors.error}`);
53 | console.log(` Context: %c ${process.env.CONTEXT} `, `padding: 2px; border-radius: 5px; background-color: ${process.env.CONTEXT === "production" ? colors.success : colors.error}`);
54 | console.log(` Location: %c ${document.location.pathname + document.location.search + document.location.hash} `, `padding: 2px; border-radius: 5px; background-color: ${colors.success}`);
55 | console.log(` User Agent: %c ${navigator.userAgent} `, `padding: 2px; border-radius: 5px; background-color: ${colors.success}`);
56 | console.log(` Branch: %c ${process.env.BRANCH} `, `padding: 2px; border-radius: 5px; background-color: ${process.env.BRANCH === "main" ? colors.success : colors.error}`);
57 | console.log(` SHA: %c ${process.env.COMMIT_REF} `, `padding: 2px; border-radius: 5px; background-color: ${colors.success}`);
58 |
59 | console.log("%cCome join us on Discord! https://discord.gg/python", `font-size: 1.5em; font-family: "Hind", "Arial"; color: ${colors.blurple}`);
60 |
61 | const rootDocument = document.getElementById("root");
62 |
63 | const root = createRoot(rootDocument!);
64 | root.render(
65 |
An error has occurred with Python Discord Forms. Please let us know in the Discord server at discord.gg/python}
67 | showDialog={true}
68 | dialogOptions={{
69 | title: "You've found a bug in PyDis forms!"
70 | }}
71 | onError={(err) => {
72 | if(process.env.NODE_ENV === "development")
73 | console.log(err);
74 | }}
75 | >
76 |
77 |
78 |
79 |
80 | );
81 |
82 | serviceWorker.unregister();
83 |
--------------------------------------------------------------------------------
/src/pages/CallbackPage.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import { jsx } from "@emotion/react";
3 | import { useState } from "react";
4 |
5 | export default function CallbackPage(): JSX.Element {
6 | const [hasSent, setHasSent] = useState(false);
7 | const params = new URLSearchParams(location.search);
8 |
9 | const code = params.get("code");
10 | const state = params.get("state");
11 |
12 | if (!hasSent) {
13 | setHasSent(true);
14 | window.opener.postMessage({code: code, state: state, pydis_source: "oauth2_callback"});
15 | }
16 |
17 | return
;
18 | }
19 |
--------------------------------------------------------------------------------
/src/pages/FormPage/ErrorPage.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | import {jsx, css} from "@emotion/react";
3 | import {Link} from "react-router-dom";
4 | import React from "react";
5 |
6 | import HeaderBar from "../../components/HeaderBar";
7 |
8 | import {Form} from "../../api/forms";
9 | import {clearAuth} from "../../api/auth";
10 | import * as styles from "../../commonStyles";
11 |
12 |
13 | interface ErrorProps {
14 | form: Form
15 | message: string
16 | }
17 |
18 | const refreshStyles = css`
19 | padding: 0.55rem 4.25rem;
20 | `;
21 |
22 |
23 | export default function ErrorPage(props: ErrorProps): JSX.Element {
24 | clearAuth();
25 |
26 | return (
27 |
28 |
29 |
30 |
{props.message}
31 |
32 |
Return Home
33 |
34 |
40 |
41 |
42 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/src/pages/FormPage/FormPage.tsx:
--------------------------------------------------------------------------------
1 | /** @jsx jsx */
2 | /** @jsxFrag */
3 | import {jsx, css} from "@emotion/react";
4 |
5 | import React, {createRef, SyntheticEvent, useEffect, useState} from "react";
6 | import {useParams} from "react-router";
7 | import {PropagateLoader} from "react-spinners";
8 | import {AxiosError} from "axios";
9 |
10 | import HeaderBar from "../../components/HeaderBar";
11 | import RenderedQuestion from "../../components/Question";
12 | import Loading from "../../components/Loading";
13 | import ScrollToTop from "../../components/ScrollToTop";
14 |
15 | import {Form, FormFeatures, getForm} from "../../api/forms";
16 | import {OAuthScopes} from "../../api/auth";
17 | import colors from "../../colors";
18 | import {unselectable} from "../../commonStyles";
19 |
20 | import handleSubmit, {FormState} from "./submit";
21 | import Navigation from "./Navigation";
22 | import Success from "./SuccessPage";
23 | import ErrorPage from "./ErrorPage";
24 | import NotFound from "../NotFound";
25 |
26 |
27 | export type RefMapType = Map
>;
28 |
29 |
30 | const formStyles = css`
31 | margin: auto;
32 | width: 50%;
33 |
34 | @media (max-width: 800px) {
35 | /* Make form larger on mobile and tablet screens */
36 | width: 80%;
37 | }
38 | `;
39 |
40 | const closedHeaderStyles = css`
41 | margin-bottom: 2rem;
42 | padding: 1rem 4rem;
43 | border-radius: 8px;
44 |
45 | text-align: center;
46 | font-size: 1.5rem;
47 |
48 | background-color: ${colors.error};
49 |
50 | @media (max-width: 500px) {
51 | padding: 1rem 1.5rem;
52 | }
53 | `;
54 |
55 | enum LoadingState {
56 | Pending,
57 | Found,
58 | Missing
59 | }
60 |
61 | function FormPage(): JSX.Element {
62 | const {id} = useParams<{id: string}>();
63 |
64 | const [form, setForm] = useState