1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-21 18:02:32 +01:00

change default instance url + formatting

This commit is contained in:
Puyodead1 2023-04-10 16:51:14 -04:00
parent 28784bec9e
commit 092de4eac5
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
27 changed files with 26372 additions and 21845 deletions

44
.editorconfig Normal file
View File

@ -0,0 +1,44 @@
root = true
[*]
charset = utf-8
# end_of_line = lf
# indent_size = 4
indent_style = tab
insert_final_newline = true
# max_line_length = 120
# tab_width = 4
[*.less]
# indent_size = 2
[*.sass]
# indent_size = 2
[*.scss]
# indent_size = 2
[*.vue]
# indent_style = tabq
[{*.ats,*.cts,*.mts,*.ts}]
# indent_style = tab
[{*.bash,*.sh,*.zsh}]
# indent_size = 2
# tab_width = 2
[{*.cjs,*.js}]
# indent_style = tab
[{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.prettierrc,.stylelintrc,bowerrc,jest.config}]
# indent_size = 2
[{*.htm,*.html,*.sht,*.shtm,*.shtml}]
# indent_style = tab
[{*.http,*.rest}]
# indent_size = 0
[{*.yaml,*.yml}]
# indent_size = 2

View File

@ -1,5 +1,5 @@
const CracoEsbuildPlugin = require("craco-esbuild"); const CracoEsbuildPlugin = require("craco-esbuild");
module.exports = { module.exports = {
plugins: [{ plugin: CracoEsbuildPlugin }], plugins: [{ plugin: CracoEsbuildPlugin }],
}; };

33974
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +1,69 @@
{ {
"name": "spacebar-client-react", "name": "spacebar-client-react",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.6", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@fontsource/roboto": "^4.5.8", "@fontsource/roboto": "^4.5.8",
"@mattjennings/react-modal-stack": "^1.0.4", "@mattjennings/react-modal-stack": "^1.0.4",
"@mui/material": "^5.11.13", "@mui/material": "^5.11.13",
"@puyodead1/fosscord-ts": "github:Puyodead1/fosscord.ts", "@puyodead1/fosscord-ts": "github:Puyodead1/fosscord.ts",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^16.18.16", "@types/node": "^16.18.16",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"mobx": "^6.8.0", "mobx": "^6.8.0",
"mobx-react-lite": "^3.4.3", "mobx-react-lite": "^3.4.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-advanced-cropper": "^0.18.0", "react-advanced-cropper": "^0.18.0",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-hook-form": "^7.43.7", "react-hook-form": "^7.43.7",
"react-icons": "^4.8.0", "react-icons": "^4.8.0",
"react-loading-skeleton": "^3.2.0", "react-loading-skeleton": "^3.2.0",
"react-router-dom": "^6.9.0", "react-router-dom": "^6.9.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-secure-storage": "^1.2.0", "react-secure-storage": "^1.2.0",
"react-select-search": "^4.1.6", "react-select-search": "^4.1.6",
"react-spinners": "^0.13.8", "react-spinners": "^0.13.8",
"reoverlay": "^1.0.3", "reoverlay": "^1.0.3",
"slate": "^0.91.4", "slate": "^0.91.4",
"slate-react": "^0.92.0", "slate-react": "^0.92.0",
"styled-components": "^5.3.9", "styled-components": "^5.3.9",
"typescript": "^4.9.5" "typescript": "^4.9.5"
}, },
"scripts": { "scripts": {
"start": "craco start", "start": "craco start",
"build": "craco build", "build": "craco build",
"test": "craco test", "test": "craco test",
"eject": "react-scripts eject" "eject": "react-scripts eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
] ]
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
"not dead", "not dead",
"not op_mini all" "not op_mini all"
], ],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",
"craco-esbuild": "^0.5.2" "craco-esbuild": "^0.5.2"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,29 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Web site created using create-react-app"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Spacebar</title> <title>Spacebar</title>
<style> <style>
html, html,
body, body,
#root, #root,
#root > div { #root > div {
height: 100%; height: 100%;
} }
</style> </style>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
</body> </body>
</html> </html>

View File

@ -1,25 +1,25 @@
{ {
"short_name": "Spacebar", "short_name": "Spacebar",
"name": "Spacebar", "name": "Spacebar",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16", "sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon" "type": "image/x-icon"
}, },
{ {
"src": "logo192.png", "src": "logo192.png",
"type": "image/png", "type": "image/png",
"sizes": "192x192" "sizes": "192x192"
}, },
{ {
"src": "logo512.png", "src": "logo512.png",
"type": "image/png", "type": "image/png",
"sizes": "512x512" "sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#000000", "theme_color": "#000000",
"background_color": "#ffffff" "background_color": "#ffffff"
} }

View File

@ -1,18 +1,18 @@
.App { .App {
text-align: center; text-align: center;
} }
.App-header { .App-header {
background-color: #282c34; background-color: #282c34;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: calc(10px + 2vmin); font-size: calc(10px + 2vmin);
color: white; color: white;
} }
.App-link { .App-link {
color: #61dafb; color: #61dafb;
} }

View File

@ -11,60 +11,60 @@ import RootPage from "./pages/RootPage";
import { useAppStore } from "./stores/AppStore"; import { useAppStore } from "./stores/AppStore";
function App() { function App() {
const app = useAppStore(); const app = useAppStore();
const navigate = useNavigate(); const navigate = useNavigate();
const [isLoading, setLoading] = React.useState(true); const [isLoading, setLoading] = React.useState(true);
React.useEffect(() => { React.useEffect(() => {
const token = secureLocalStorage.getItem("token"); const token = secureLocalStorage.getItem("token");
if (token) { if (token) {
app.api.loginWithToken(token as string).then(() => { app.api.loginWithToken(token as string).then(() => {
setLoading(false); setLoading(false);
}); });
} else { } else {
// set timeout to prevent flashing // set timeout to prevent flashing
setTimeout(() => { setTimeout(() => {
setLoading(false); setLoading(false);
}, 1000); }, 1000);
} }
}, []); }, []);
// handles token changes // handles token changes
React.useEffect(() => { React.useEffect(() => {
// FIXME: this triggers on load and causes a redirect to login no matter what page we actually want to go to // FIXME: this triggers on load and causes a redirect to login no matter what page we actually want to go to
// if (!app.api.token && !isLoading) { // if (!app.api.token && !isLoading) {
// console.log("TOKEN REMOVED"); // console.log("TOKEN REMOVED");
// // remove token // // remove token
// secureLocalStorage.removeItem("token"); // secureLocalStorage.removeItem("token");
// // navigate to login page if token is removed // // navigate to login page if token is removed
// navigate("/login", { replace: true }); // navigate("/login", { replace: true });
// } // }
if (app.api.token) { if (app.api.token) {
console.log("TOKEN ADDED"); console.log("TOKEN ADDED");
// save token // save token
secureLocalStorage.setItem("token", app.api.token); secureLocalStorage.setItem("token", app.api.token);
// navigate to root page if token is added // navigate to root page if token is added
navigate("/", { replace: true }); navigate("/", { replace: true });
} }
}, [app.api.token, isLoading]); }, [app.api.token, isLoading]);
if (isLoading) { if (isLoading) {
return <LoadingPage />; return <LoadingPage />;
} }
return ( return (
<Routes> <Routes>
<Route <Route
index index
path="/" path="/"
element={<AuthenticationGuard component={RootPage} />} element={<AuthenticationGuard component={RootPage} />}
/> />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegistrationPage />} /> <Route path="/register" element={<RegistrationPage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
); );
} }
export default observer(App); export default observer(App);

View File

@ -2,16 +2,16 @@ import { Navigate } from "react-router-dom";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../stores/AppStore";
interface Props { interface Props {
component: React.FC; component: React.FC;
} }
export const AuthenticationGuard = ({ component }: Props) => { export const AuthenticationGuard = ({ component }: Props) => {
const app = useAppStore(); const app = useAppStore();
if (!app.api.token) { if (!app.api.token) {
return <Navigate to="/login" replace />; return <Navigate to="/login" replace />;
} }
const Component = component; const Component = component;
return <Component />; return <Component />;
}; };

View File

@ -1,45 +1,45 @@
import styled from "styled-components"; import styled from "styled-components";
interface Props { interface Props {
variant?: "primary" | "secondary" | "danger" | "success" | "warning"; variant?: "primary" | "secondary" | "danger" | "success" | "warning";
outlined?: boolean; outlined?: boolean;
} }
export default styled.button<Props>` export default styled.button<Props>`
background: ${(props) => { background: ${(props) => {
if (props.outlined) return "transparent"; if (props.outlined) return "transparent";
switch (props.variant) { switch (props.variant) {
case "primary": case "primary":
return "var(--button-primary)"; return "var(--button-primary)";
case "secondary": case "secondary":
return "var(--button-secondary)"; return "var(--button-secondary)";
case "danger": case "danger":
return "var(--button-danger)"; return "var(--button-danger)";
case "success": case "success":
return "var(--button-success)"; return "var(--button-success)";
case "warning": case "warning":
return "var(--button-warning)"; return "var(--button-warning)";
default: default:
return "var(--button-primary)"; return "var(--button-primary)";
} }
}}; }};
border: ${(props) => { border: ${(props) => {
if (!props.outlined) return "none"; if (!props.outlined) return "none";
switch (props.variant) { switch (props.variant) {
case "primary": case "primary":
return "1px solid var(--button-primary)"; return "1px solid var(--button-primary)";
case "secondary": case "secondary":
return "1px solid var(--button-secondary)"; return "1px solid var(--button-secondary)";
case "danger": case "danger":
return "1px solid var(--button-danger)"; return "1px solid var(--button-danger)";
case "success": case "success":
return "1px solid var(--button-success)"; return "1px solid var(--button-success)";
case "warning": case "warning":
return "1px solid var(--button-warning)"; return "1px solid var(--button-warning)";
default: default:
return "1px solid var(--button-primary)"; return "1px solid var(--button-primary)";
} }
}}; }};
color: var(--text); color: var(--text);
@ -55,37 +55,37 @@ opacity: ${(props) => (props.disabled ? 0.5 : 1)};
&:hover { &:hover {
background: ${(props) => { background: ${(props) => {
switch (props.variant) { switch (props.variant) {
case "primary": case "primary":
return "var(--button-primary-hover)"; return "var(--button-primary-hover)";
case "secondary": case "secondary":
return "var(--button-secondary-hover)"; return "var(--button-secondary-hover)";
case "danger": case "danger":
return "var(--button-danger-hover)"; return "var(--button-danger-hover)";
case "success": case "success":
return "var(--button-success-hover)"; return "var(--button-success-hover)";
case "warning": case "warning":
return "var(--button-warning-hover)"; return "var(--button-warning-hover)";
default: default:
return "var(--button-primary-hover)"; return "var(--button-primary-hover)";
} }
}}; }};
&:active { &:active {
background: ${(props) => { background: ${(props) => {
switch (props.variant) { switch (props.variant) {
case "primary": case "primary":
return "var(--button-primary-active)"; return "var(--button-primary-active)";
case "secondary": case "secondary":
return "var(--button-secondary-active)"; return "var(--button-secondary-active)";
case "danger": case "danger":
return "var(--button-danger-active)"; return "var(--button-danger-active)";
case "success": case "success":
return "var(--button-success-active)"; return "var(--button-success-active)";
case "warning": case "warning":
return "var(--button-warning-active)"; return "var(--button-warning-active)";
default: default:
return "var(--button-primary-active)"; return "var(--button-primary-active)";
} }
}}; }};
`; `;

View File

@ -1,7 +1,7 @@
import styled from "styled-components"; import styled from "styled-components";
export default styled.div` export default styled.div`
background-color: var(--tertiary); background-color: var(--tertiary);
color: var(--text); color: var(--text);
overflow: hidden; overflow: hidden;
`; `;

View File

@ -1,56 +1,56 @@
.select-search-container { .select-search-container {
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.select-search-input { .select-search-input {
position: relative; position: relative;
display: block; display: block;
padding: 10px; padding: 10px;
background: var(--secondary); background: var(--secondary);
border: none; border: none;
color: var(--text); color: var(--text);
outline: none; outline: none;
font-size: 16px; font-size: 16px;
text-align: left; text-align: left;
border-radius: 12px; border-radius: 12px;
} }
.select-search-is-multiple .select-search-input { .select-search-is-multiple .select-search-input {
margin-bottom: -2px; margin-bottom: -2px;
} }
.select-search-is-multiple .select-search-input { .select-search-is-multiple .select-search-input {
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
} }
.select-search-input[readonly] { .select-search-input[readonly] {
cursor: pointer; cursor: pointer;
} }
.select-search-is-disabled .select-search-input { .select-search-is-disabled .select-search-input {
cursor: not-allowed; cursor: not-allowed;
} }
.select-search-select { .select-search-select {
border: none; border: none;
overflow: auto; overflow: auto;
max-height: 230px; max-height: 230px;
position: relative; position: relative;
bottom: 4vh; bottom: 4vh;
} }
.select-search-container:not(.select-search-is-multiple) .select-search-select { .select-search-container:not(.select-search-is-multiple) .select-search-select {
position: absolute; position: absolute;
right: 0; right: 0;
left: 0; left: 0;
border-radius: 3px; border-radius: 3px;
display: none; display: none;
} }
.select-search-container:not(.select-search-is-multiple).select-search-has-focus .select-search-container:not(.select-search-is-multiple).select-search-has-focus
.select-search-select { .select-search-select {
display: block; display: block;
} }
/* .select-search-has-focus .select-search-select { /* .select-search-has-focus .select-search-select {
@ -58,48 +58,48 @@
} */ } */
.select-search-options { .select-search-options {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.select-search-option, .select-search-option,
.select-search-not-found { .select-search-not-found {
display: block; display: block;
height: 42px; height: 42px;
width: 100%; width: 100%;
padding: 0 16px; padding: 0 16px;
background: var(--primary); background: var(--primary);
border: none; border: none;
outline: none; outline: none;
color: var(--text); color: var(--text);
font-size: 16px; font-size: 16px;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
margin: 0; margin: 0;
} }
.select-search-option:disabled { .select-search-option:disabled {
opacity: 0.5; opacity: 0.5;
cursor: not-allowed; cursor: not-allowed;
background: transparent !important; background: transparent !important;
} }
.select-search-is-highlighted, .select-search-is-highlighted,
.select-search-option:not(.select-search-is-selected):hover { .select-search-option:not(.select-search-is-selected):hover {
background: hsl(20, 4%, 20%); background: hsl(20, 4%, 20%);
} }
.select-search-is-selected { .select-search-is-selected {
font-weight: bold; font-weight: bold;
color: var(--text); color: var(--text);
background-color: hsl(20, 4%, 25%); background-color: hsl(20, 4%, 25%);
} }
.select-search-row:not(:first-child) .select-search-group-header { .select-search-row:not(:first-child) .select-search-group-header {
margin-top: 10px; margin-top: 10px;
} }
.select-search-row:not(:last-child) .select-search-group-header { .select-search-row:not(:last-child) .select-search-group-header {
margin-bottom: 10px; margin-bottom: 10px;
} }

View File

@ -8,209 +8,209 @@ const MIN_AGE = 3; // we do this instead so we can show an age gate if they are
const MAX_AGE = 120; const MAX_AGE = 120;
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
`; `;
const Input = styled.input<{ error?: boolean }>` const Input = styled.input<{ error?: boolean }>`
outline: none; outline: none;
background: var(--secondary); background: var(--secondary);
padding: 10px; padding: 10px;
font-size: 16px; font-size: 16px;
border-radius: 12px; border-radius: 12px;
color: var(--text); color: var(--text);
margin: 0 0 0 5px; margin: 0 0 0 5px;
border: none; border: none;
aria-invalid: ${(props) => (props.error ? "true" : "false")}; aria-invalid: ${(props) => (props.error ? "true" : "false")};
border: ${(props) => (props.error ? "1px solid red" : "none")}; border: ${(props) => (props.error ? "1px solid red" : "none")};
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
`; `;
const MONTHS = [ const MONTHS = [
{ {
value: "01", value: "01",
name: "January", name: "January",
}, },
{ {
value: "02", value: "02",
name: "February", name: "February",
}, },
{ {
value: "03", value: "03",
name: "March", name: "March",
}, },
{ {
value: "04", value: "04",
name: "April", name: "April",
}, },
{ {
value: "05", value: "05",
name: "May", name: "May",
}, },
{ {
value: "06", value: "06",
name: "June", name: "June",
}, },
{ {
value: "07", value: "07",
name: "July", name: "July",
}, },
{ {
value: "08", value: "08",
name: "August", name: "August",
}, },
{ {
value: "09", value: "09",
name: "September", name: "September",
}, },
{ {
value: "10", value: "10",
name: "October", name: "October",
}, },
{ {
value: "11", value: "11",
name: "November", name: "November",
}, },
{ {
value: "12", value: "12",
name: "December", name: "December",
}, },
]; ];
export class DOBInput extends Component< export class DOBInput extends Component<
{ {
onChange: (value: string) => void; onChange: (value: string) => void;
onErrorChange: (errors: { onErrorChange: (errors: {
month?: string; month?: string;
day?: string; day?: string;
year?: string; year?: string;
}) => void; }) => void;
error: boolean; error: boolean;
}, },
{ {
month?: string; month?: string;
day?: string; day?: string;
year?: string; year?: string;
errors: { month?: string; day?: string; year?: string }; errors: { month?: string; day?: string; year?: string };
} }
> { > {
state = { state = {
month: "", month: "",
day: "", day: "",
year: "", year: "",
errors: { errors: {
month: undefined, month: undefined,
day: undefined, day: undefined,
year: undefined, year: undefined,
}, },
}; };
componentDidUpdate(prevProps: any, prevState: any) { componentDidUpdate(prevProps: any, prevState: any) {
if (prevState !== this.state) { if (prevState !== this.state) {
this.props.onErrorChange(this.state.errors); this.props.onErrorChange(this.state.errors);
this.props.onChange( this.props.onChange(
this.constructDate({ this.constructDate({
month: this.state.month, month: this.state.month,
day: this.state.day, day: this.state.day,
year: this.state.year, year: this.state.year,
}) })
); );
} }
} }
onInputChange = onInputChange =
(type: string) => (e: React.ChangeEvent<HTMLInputElement>) => { (type: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value; const value = e.target.value;
// clear error for field // clear error for field
this.setState( this.setState(
{ {
...this.state, ...this.state,
errors: { ...this.state.errors, [type]: undefined }, errors: { ...this.state.errors, [type]: undefined },
}, },
() => { () => {
// ensure only numbers // ensure only numbers
if (isNaN(Number(value))) { if (isNaN(Number(value))) {
this.setState({ this.setState({
...this.state, ...this.state,
errors: { ...this.state.errors, [type]: "Invalid Date" }, errors: { ...this.state.errors, [type]: "Invalid Date" },
}); });
return; return;
} }
if (type === "day") { if (type === "day") {
// day should be a number between 1-31 and not more than 2 digits // day should be a number between 1-31 and not more than 2 digits
if ( if (
value !== "" && value !== "" &&
(value.length > 2 || Number(value) > 31 || Number(value) < 1) (value.length > 2 || Number(value) > 31 || Number(value) < 1)
) { ) {
this.setState({ this.setState({
...this.state, ...this.state,
day: value, day: value,
errors: { ...this.state.errors, [type]: "Invalid Date" }, errors: { ...this.state.errors, [type]: "Invalid Date" },
}); });
return; return;
} }
this.setState({ ...this.state, day: value }); this.setState({ ...this.state, day: value });
} }
if (type === "year") { if (type === "year") {
// year must be between now-min and now-max // year must be between now-min and now-max
if ( if (
value.length === 4 && value.length === 4 &&
(Number(value) > new Date().getFullYear() - MIN_AGE || (Number(value) > new Date().getFullYear() - MIN_AGE ||
Number(value) < new Date().getFullYear() - MAX_AGE) Number(value) < new Date().getFullYear() - MAX_AGE)
) { ) {
this.setState({ this.setState({
...this.state, ...this.state,
year: value, year: value,
errors: { ...this.state.errors, [type]: "Invalid Date" }, errors: { ...this.state.errors, [type]: "Invalid Date" },
}); });
return; return;
} }
this.setState({ ...this.state, year: value }); this.setState({ ...this.state, year: value });
} }
} }
); );
}; };
constructDate = (values: { month: string; day: string; year: string }) => { constructDate = (values: { month: string; day: string; year: string }) => {
const { month, day, year } = values; const { month, day, year } = values;
// pad day with 0 if needed // pad day with 0 if needed
const dayPadded = day?.length === 1 ? `0${day}` : day; const dayPadded = day?.length === 1 ? `0${day}` : day;
return `${year}-${month}-${dayPadded}`; return `${year}-${month}-${dayPadded}`;
}; };
render() { render() {
return ( return (
<Container> <Container>
<SelectSearch <SelectSearch
placeholder="Month" placeholder="Month"
search search
options={MONTHS} options={MONTHS}
onChange={(e) => this.setState({ ...this.state, month: e as string })} onChange={(e) => this.setState({ ...this.state, month: e as string })}
value={this.state.month} value={this.state.month}
/> />
<Input <Input
placeholder="Day" placeholder="Day"
onChange={this.onInputChange("day")} onChange={this.onInputChange("day")}
value={this.state.day} value={this.state.day}
error={this.state.errors.day || this.props.error} error={this.state.errors.day || this.props.error}
maxLength={2} maxLength={2}
/> />
<Input <Input
placeholder="Year" placeholder="Year"
onChange={this.onInputChange("year")} onChange={this.onInputChange("year")}
value={this.state.year} value={this.state.year}
error={this.state.errors.year || this.props.error} error={this.state.errors.year || this.props.error}
maxLength={4} maxLength={4}
/> />
</Container> </Container>
); );
} }
} }
export default DOBInput; export default DOBInput;

View File

@ -1,5 +1,5 @@
import styled from "styled-components"; import styled from "styled-components";
export default styled.div` export default styled.div`
color: var(--text); color: var(--text);
`; `;

View File

@ -3,99 +3,99 @@ import { createGlobalStyle } from "styled-components";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../stores/AppStore";
export type ThemeVariables = export type ThemeVariables =
| "brandPrimary" | "brandPrimary"
| "brandSecondary" | "brandSecondary"
| "primary" | "primary"
| "primaryAlt" | "primaryAlt"
| "secondary" | "secondary"
| "tertiary" | "tertiary"
| "text" | "text"
| "textMuted" | "textMuted"
| "textLink" | "textLink"
| "inputBackground" | "inputBackground"
| "error" | "error"
| "buttonPrimary" | "buttonPrimary"
| "buttonPrimaryHover" | "buttonPrimaryHover"
| "buttonPrimaryActive" | "buttonPrimaryActive"
| "buttonSecondary" | "buttonSecondary"
| "buttonSecondaryHover" | "buttonSecondaryHover"
| "buttonSecondaryActive" | "buttonSecondaryActive"
| "buttonDanger" | "buttonDanger"
| "buttonDangerHover" | "buttonDangerHover"
| "buttonDangerActive" | "buttonDangerActive"
| "buttonSuccess" | "buttonSuccess"
| "buttonSuccessHover" | "buttonSuccessHover"
| "buttonSuccessActive" | "buttonSuccessActive"
| "buttonWarning" | "buttonWarning"
| "buttonWarningHover" | "buttonWarningHover"
| "buttonWarningActive"; | "buttonWarningActive";
export type Overrides = { export type Overrides = {
[variable in ThemeVariables]: string; [variable in ThemeVariables]: string;
}; };
export type Theme = Overrides & { export type Theme = Overrides & {
light?: boolean; light?: boolean;
}; };
export const ThemePresets: Record<string, Theme> = { export const ThemePresets: Record<string, Theme> = {
light: { light: {
brandPrimary: "#0185ff", brandPrimary: "#0185ff",
brandSecondary: "#ffffff", brandSecondary: "#ffffff",
primary: "#ede8e7", primary: "#ede8e7",
primaryAlt: "", primaryAlt: "",
secondary: "#ebe5e4", secondary: "#ebe5e4",
tertiary: "#e9e2e1", tertiary: "#e9e2e1",
text: "#000000", text: "#000000",
textMuted: "#232120", textMuted: "#232120",
textLink: "#00a8fc", textLink: "#00a8fc",
inputBackground: "#757575", inputBackground: "#757575",
error: "#e83f36", error: "#e83f36",
buttonPrimary: "", buttonPrimary: "",
buttonPrimaryHover: "", buttonPrimaryHover: "",
buttonPrimaryActive: "", buttonPrimaryActive: "",
buttonSecondary: "", buttonSecondary: "",
buttonSecondaryHover: "", buttonSecondaryHover: "",
buttonSecondaryActive: "", buttonSecondaryActive: "",
buttonDanger: "", buttonDanger: "",
buttonDangerHover: "", buttonDangerHover: "",
buttonDangerActive: "", buttonDangerActive: "",
buttonSuccess: "", buttonSuccess: "",
buttonSuccessHover: "", buttonSuccessHover: "",
buttonSuccessActive: "", buttonSuccessActive: "",
buttonWarning: "", buttonWarning: "",
buttonWarningHover: "", buttonWarningHover: "",
buttonWarningActive: "", buttonWarningActive: "",
}, },
dark: { dark: {
brandPrimary: "#0185ff", brandPrimary: "#0185ff",
brandSecondary: "#ffffff", brandSecondary: "#ffffff",
primary: "#232120", primary: "#232120",
primaryAlt: "#312e2d", primaryAlt: "#312e2d",
secondary: "#1b1918", secondary: "#1b1918",
tertiary: "#141212", tertiary: "#141212",
text: "#e9e2e1", text: "#e9e2e1",
textMuted: "#85898f", textMuted: "#85898f",
textLink: "#00a8fc", textLink: "#00a8fc",
inputBackground: "#121212", inputBackground: "#121212",
error: "#e83f36", error: "#e83f36",
// buttons // buttons
buttonPrimary: "#0185ff", buttonPrimary: "#0185ff",
buttonPrimaryHover: "#0078e6", buttonPrimaryHover: "#0078e6",
buttonPrimaryActive: "#006acd", buttonPrimaryActive: "#006acd",
buttonSecondary: "#4a4544", buttonSecondary: "#4a4544",
buttonSecondaryHover: "#746d69", buttonSecondaryHover: "#746d69",
buttonSecondaryActive: "#5f5a59", buttonSecondaryActive: "#5f5a59",
buttonDanger: "#ff3a3b", buttonDanger: "#ff3a3b",
buttonDangerHover: "#ff2d2f", buttonDangerHover: "#ff2d2f",
buttonDangerActive: "#ff2425", buttonDangerActive: "#ff2425",
buttonSuccess: "#34af65", buttonSuccess: "#34af65",
buttonSuccessHover: "#31a660", buttonSuccessHover: "#31a660",
buttonSuccessActive: "#2d9657", buttonSuccessActive: "#2d9657",
buttonWarning: "#faa61a", buttonWarning: "#faa61a",
buttonWarningHover: "#e69105", buttonWarningHover: "#e69105",
buttonWarningActive: "#c27b04", buttonWarningActive: "#c27b04",
}, },
}; };
const GlobalTheme = createGlobalStyle<{ theme: Theme }>` const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
@ -105,29 +105,29 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
`; `;
const toDashed = (str: string) => const toDashed = (str: string) =>
str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase()); str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
export const generateVariables = (theme: Theme) => { export const generateVariables = (theme: Theme) => {
return (Object.keys(theme) as ThemeVariables[]).map((key) => { return (Object.keys(theme) as ThemeVariables[]).map((key) => {
const colour = theme[key]; const colour = theme[key];
try { try {
const r = parseInt(colour.substring(1, 3), 16); const r = parseInt(colour.substring(1, 3), 16);
const g = parseInt(colour.substring(3, 5), 16); const g = parseInt(colour.substring(3, 5), 16);
const b = parseInt(colour.substring(5, 7), 16); const b = parseInt(colour.substring(5, 7), 16);
return `--${toDashed(key)}: ${theme[key]}; --${toDashed( return `--${toDashed(key)}: ${theme[key]}; --${toDashed(
key key
)}-rgb: rgb(${r}, ${g}, ${b});`; )}-rgb: rgb(${r}, ${g}, ${b});`;
} catch { } catch {
return `--${toDashed(key)}: ${theme[key]};`; return `--${toDashed(key)}: ${theme[key]};`;
} }
}); });
}; };
export default observer(() => { export default observer(() => {
const appStore = useAppStore(); const appStore = useAppStore();
const theme = appStore.theme; const theme = appStore.theme;
const variables = theme.computeVariables(); const variables = theme.computeVariables();
return <GlobalTheme theme={variables} />; return <GlobalTheme theme={variables} />;
}); });

View File

@ -1,13 +1,13 @@
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif; sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace; monospace;
} }

View File

@ -9,11 +9,11 @@ import Theme from "./contexts/Theme";
import "./index.css"; import "./index.css";
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement document.getElementById("root") as HTMLElement
); );
root.render( root.render(
<BrowserRouter> <BrowserRouter>
<App /> <App />
<Theme /> <Theme />
</BrowserRouter> </BrowserRouter>
); );

View File

@ -2,16 +2,16 @@ import Container from "../components/Container";
import Text from "../components/Text"; import Text from "../components/Text";
interface Props { interface Props {
error: Error; error: Error;
} }
function ErrorPage({ error }: Props) { function ErrorPage({ error }: Props) {
return ( return (
<Container> <Container>
<Text>Oops, Something went wrong!</Text> <Text>Oops, Something went wrong!</Text>
<pre>{error.message}</pre> <pre>{error.message}</pre>
</Container> </Container>
); );
} }
export default ErrorPage; export default ErrorPage;

View File

@ -5,27 +5,27 @@ import { ReactComponent as SpacebarLogoBlue } from "../assets/images/logo/Logo-B
import Container from "../components/Container"; import Container from "../components/Container";
const Wrapper = styled.div` const Wrapper = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
display: flex; display: flex;
height: 100vh; height: 100vh;
flex-direction: column; flex-direction: column;
`; `;
const SpacebarLogo = styled(SpacebarLogoBlue)` const SpacebarLogo = styled(SpacebarLogoBlue)`
height: 120px; height: 120px;
margin-bottom: 32px; margin-bottom: 32px;
`; `;
function LoadingPage() { function LoadingPage() {
return ( return (
<Container> <Container>
<Wrapper> <Wrapper>
<SpacebarLogo /> <SpacebarLogo />
<PulseLoader color="var(--brand-secondary)" /> <PulseLoader color="var(--brand-secondary)" />
</Wrapper> </Wrapper>
</Container> </Container>
); );
} }
export default observer(LoadingPage); export default observer(LoadingPage);

View File

@ -7,261 +7,260 @@ import Container from "../components/Container";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../stores/AppStore";
const Wrapper = styled(Container)` const Wrapper = styled(Container)`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; height: 100vh;
background-color: var(--secondary); background-color: var(--secondary);
`; `;
const LoginBox = styled(Container)` const LoginBox = styled(Container)`
background-color: var(--primary-alt); background-color: var(--primary-alt);
padding: 32px; padding: 32px;
font-size: 18px; font-size: 18px;
color: var(--text-muted); color: var(--text-muted);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center; text-align: center;
@media (max-width: 480px) { @media (max-width: 480px) {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@media (min-width: 480px) { @media (min-width: 480px) {
width: 480px; width: 480px;
border-radius: 18px; border-radius: 18px;
} }
`; `;
const HeaderContainer = styled.div` const HeaderContainer = styled.div`
width: 100%; width: 100%;
`; `;
const Header = styled.h1` const Header = styled.h1`
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 24px; font-size: 24px;
color: var(--text); color: var(--text);
`; `;
const SubHeader = styled.h2` const SubHeader = styled.h2`
color: var(--text-muted); color: var(--text-muted);
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
`; `;
const FormContainer = styled.form` const FormContainer = styled.form`
width: 100%; width: 100%;
`; `;
const InputContainer = styled.h1<{ marginBottom: boolean }>` const InputContainer = styled.h1<{ marginBottom: boolean }>`
margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")}; margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
`; `;
const LabelWrapper = styled.div<{ error?: boolean }>` const LabelWrapper = styled.div<{ error?: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 8px; margin-bottom: 8px;
color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")}; color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")};
`; `;
const InputErrorText = styled.label` const InputErrorText = styled.label`
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
`; `;
const InputLabel = styled.label` const InputLabel = styled.label`
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
`; `;
const InputWrapper = styled.div` const InputWrapper = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
`; `;
const Input = styled.input<{ error?: boolean }>` const Input = styled.input<{ error?: boolean }>`
outline: none; outline: none;
background: var(--secondary); background: var(--secondary);
padding: 10px; padding: 10px;
font-size: 16px; font-size: 16px;
flex: 1; flex: 1;
border-radius: 12px; border-radius: 12px;
color: var(--text); color: var(--text);
margin: 0; margin: 0;
border: none; border: none;
aria-invalid: ${(props) => (props.error ? "true" : "false")}; aria-invalid: ${(props) => (props.error ? "true" : "false")};
`; `;
const PasswordResetLink = styled.button` const PasswordResetLink = styled.button`
margin-bottom: 20px; margin-bottom: 20px;
margin-top: 4px; margin-top: 4px;
padding: 2px 0; padding: 2px 0;
font-size: 14px; font-size: 14px;
display: flex; display: flex;
color: var(--text-link); color: var(--text-link);
background: none; background: none;
border: none; border: none;
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
`; `;
const LoginButton = styled(Button)` const LoginButton = styled(Button)`
margin-bottom: 8px; margin-bottom: 8px;
width: 100%; width: 100%;
min-width: 130px; min-width: 130px;
min-height: 44px; min-height: 44px;
`; `;
const RegisterContainer = styled.div` const RegisterContainer = styled.div`
margin-top: 4px; margin-top: 4px;
text-align: initial; text-align: initial;
`; `;
const RegisterLabel = styled.label` const RegisterLabel = styled.label`
font-size: 14px; font-size: 14px;
`; `;
const RegisterLink = styled.button` const RegisterLink = styled.button`
font-size: 14px; font-size: 14px;
background: none; background: none;
border: none; border: none;
color: var(--text-link); color: var(--text-link);
@media (max-width: 480px) { @media (max-width: 480px) {
display: inline-block; display: inline-block;
} }
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
`; `;
const Divider = styled.span` const Divider = styled.span`
padding: 0 4px; padding: 0 4px;
`; `;
type LoginFormValues = { type LoginFormValues = {
login: string; login: string;
password: string; password: string;
}; };
function LoginPage() { function LoginPage() {
const app = useAppStore(); const app = useAppStore();
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
register, register,
handleSubmit, handleSubmit,
watch, formState: { errors },
formState: { errors }, setError,
setError, } = useForm<LoginFormValues>();
} = useForm<LoginFormValues>();
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
app.api.login(data).catch((e) => { app.api.login(data).catch((e) => {
if (e instanceof MFAError) { if (e instanceof MFAError) {
console.log("MFA Required", e); console.log("MFA Required", e);
} else if (e instanceof CaptchaError) { } else if (e instanceof CaptchaError) {
console.log("Captcha Required", e); console.log("Captcha Required", e);
} else if (e instanceof APIError) { } else if (e instanceof APIError) {
console.log("APIError", e.message, e.code, e.fieldErrors); console.log("APIError", e.message, e.code, e.fieldErrors);
e.fieldErrors.forEach((fieldError) => { e.fieldErrors.forEach((fieldError) => {
setError(fieldError.field as any, { setError(fieldError.field as any, {
type: "manual", type: "manual",
message: fieldError.error, message: fieldError.error,
}); });
}); });
} else { } else {
console.log("General Error", e); console.log("General Error", e);
} }
}); });
}); });
return ( return (
<Wrapper> <Wrapper>
<LoginBox> <LoginBox>
<HeaderContainer> <HeaderContainer>
<Header>Welcome Back!</Header> <Header>Welcome Back!</Header>
<SubHeader>We're so excited to see you again!</SubHeader> <SubHeader>We're so excited to see you again!</SubHeader>
</HeaderContainer> </HeaderContainer>
<FormContainer onSubmit={onSubmit}> <FormContainer onSubmit={onSubmit}>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}> <InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.login}> <LabelWrapper error={!!errors.login}>
<InputLabel>Email</InputLabel> <InputLabel>Email</InputLabel>
{errors.login && ( {errors.login && (
<InputErrorText> <InputErrorText>
<> <>
<Divider>-</Divider> <Divider>-</Divider>
{errors.login.message} {errors.login.message}
</> </>
</InputErrorText> </InputErrorText>
)} )}
</LabelWrapper> </LabelWrapper>
<InputWrapper> <InputWrapper>
<Input <Input
type="email" type="email"
autoFocus autoFocus
{...register("login", { required: true })} {...register("login", { required: true })}
error={!!errors.login} error={!!errors.login}
/> />
</InputWrapper> </InputWrapper>
</InputContainer> </InputContainer>
<InputContainer marginBottom={false}> <InputContainer marginBottom={false}>
<LabelWrapper error={!!errors.password}> <LabelWrapper error={!!errors.password}>
<InputLabel>Password</InputLabel> <InputLabel>Password</InputLabel>
{errors.password && ( {errors.password && (
<InputErrorText> <InputErrorText>
<> <>
<Divider>-</Divider> <Divider>-</Divider>
{errors.password.message} {errors.password.message}
</> </>
</InputErrorText> </InputErrorText>
)} )}
</LabelWrapper> </LabelWrapper>
<InputWrapper> <InputWrapper>
<Input <Input
type="password" type="password"
{...register("password", { required: true })} {...register("password", { required: true })}
error={!!errors.password} error={!!errors.password}
/> />
</InputWrapper> </InputWrapper>
</InputContainer> </InputContainer>
<PasswordResetLink onClick={() => {}} type="button"> <PasswordResetLink onClick={() => {}} type="button">
Forgot your password? Forgot your password?
</PasswordResetLink> </PasswordResetLink>
<LoginButton variant="primary" type="submit"> <LoginButton variant="primary" type="submit">
Log In Log In
</LoginButton> </LoginButton>
<RegisterContainer> <RegisterContainer>
<RegisterLabel>Don't have an account?&nbsp;</RegisterLabel> <RegisterLabel>Don't have an account?&nbsp;</RegisterLabel>
<RegisterLink <RegisterLink
onClick={() => { onClick={() => {
navigate("/register"); navigate("/register");
}} }}
type="button" type="button"
> >
Sign Up Sign Up
</RegisterLink> </RegisterLink>
</RegisterContainer> </RegisterContainer>
</FormContainer> </FormContainer>
</LoginBox> </LoginBox>
</Wrapper> </Wrapper>
); );
} }
export default LoginPage; export default LoginPage;

View File

@ -2,11 +2,11 @@ import Container from "../components/Container";
import Text from "../components/Text"; import Text from "../components/Text";
function NotFoundPage() { function NotFoundPage() {
return ( return (
<Container> <Container>
<Text>NotFound</Text> <Text>NotFound</Text>
</Container> </Container>
); );
} }
export default NotFoundPage; export default NotFoundPage;

View File

@ -8,299 +8,299 @@ import DOBInput from "../components/DOBInput";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../stores/AppStore";
const Wrapper = styled(Container)` const Wrapper = styled(Container)`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; height: 100vh;
background-color: var(--secondary); background-color: var(--secondary);
`; `;
const LoginBox = styled(Container)` const LoginBox = styled(Container)`
background-color: var(--primary-alt); background-color: var(--primary-alt);
padding: 32px; padding: 32px;
font-size: 18px; font-size: 18px;
color: var(--text-muted); color: var(--text-muted);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center; text-align: center;
@media (max-width: 480px) { @media (max-width: 480px) {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
@media (min-width: 480px) { @media (min-width: 480px) {
width: 480px; width: 480px;
border-radius: 18px; border-radius: 18px;
} }
`; `;
const HeaderContainer = styled.div` const HeaderContainer = styled.div`
width: 100%; width: 100%;
`; `;
const Header = styled.h1` const Header = styled.h1`
font-weight: 600; font-weight: 600;
margin-bottom: 8px; margin-bottom: 8px;
font-size: 24px; font-size: 24px;
color: var(--text); color: var(--text);
`; `;
const SubHeader = styled.h2` const SubHeader = styled.h2`
color: var(--text-muted); color: var(--text-muted);
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
`; `;
const FormContainer = styled.form` const FormContainer = styled.form`
width: 100%; width: 100%;
`; `;
const InputContainer = styled.h1<{ marginBottom: boolean }>` const InputContainer = styled.h1<{ marginBottom: boolean }>`
margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")}; margin-bottom: ${(props) => (props.marginBottom ? "20px" : "0")};
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
`; `;
const LabelWrapper = styled.div<{ error?: boolean }>` const LabelWrapper = styled.div<{ error?: boolean }>`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 8px; margin-bottom: 8px;
color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")}; color: ${(props) => (props.error ? "var(--error)" : "#b1b5bc")};
`; `;
const InputErrorText = styled.label` const InputErrorText = styled.label`
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
`; `;
const InputLabel = styled.label` const InputLabel = styled.label`
font-size: 14px; font-size: 14px;
font-weight: 700; font-weight: 700;
`; `;
const InputWrapper = styled.div` const InputWrapper = styled.div`
width: 100%; width: 100%;
display: flex; display: flex;
`; `;
const Input = styled.input<{ error?: boolean }>` const Input = styled.input<{ error?: boolean }>`
outline: none; outline: none;
background: var(--secondary); background: var(--secondary);
padding: 10px; padding: 10px;
font-size: 16px; font-size: 16px;
border-radius: 12px; border-radius: 12px;
color: var(--text); color: var(--text);
margin: 0; margin: 0;
border: none; border: none;
aria-invalid: ${(props) => (props.error ? "true" : "false")}; aria-invalid: ${(props) => (props.error ? "true" : "false")};
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
`; `;
const LoginButton = styled(Button)` const LoginButton = styled(Button)`
margin-bottom: 8px; margin-bottom: 8px;
width: 100%; width: 100%;
min-width: 130px; min-width: 130px;
min-height: 44px; min-height: 44px;
`; `;
const LoginLink = styled.button` const LoginLink = styled.button`
margin-top: 4px; margin-top: 4px;
float: left; float: left;
font-size: 14px; font-size: 14px;
background: none; background: none;
border: none; border: none;
color: var(--text-link); color: var(--text-link);
@media (max-width: 480px) { @media (max-width: 480px) {
display: inline-block; display: inline-block;
} }
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
`; `;
const Divider = styled.span` const Divider = styled.span`
padding: 0 4px; padding: 0 4px;
`; `;
type RegisterFormValues = { type RegisterFormValues = {
email: string; email: string;
username: string; username: string;
password: string; password: string;
date_of_birth: string; date_of_birth: string;
}; };
function RegistrationPage() { function RegistrationPage() {
const app = useAppStore(); const app = useAppStore();
const navigate = useNavigate(); const navigate = useNavigate();
const { const {
register, register,
handleSubmit, handleSubmit,
setValue, setValue,
formState: { errors }, formState: { errors },
setError, setError,
clearErrors, clearErrors,
} = useForm<RegisterFormValues>(); } = useForm<RegisterFormValues>();
const dobRegister = register("date_of_birth", { const dobRegister = register("date_of_birth", {
required: true, required: true,
pattern: /^\d{4}-\d{2}-\d{2}$/, pattern: /^\d{4}-\d{2}-\d{2}$/,
}); });
const onSubmit = handleSubmit((data) => { const onSubmit = handleSubmit((data) => {
app.api app.api
.register({ .register({
...data, ...data,
consent: true, consent: true,
}) })
.catch((e) => { .catch((e) => {
if (e instanceof CaptchaError) { if (e instanceof CaptchaError) {
console.log("Captcha Required", e); console.log("Captcha Required", e);
} else if (e instanceof APIError) { } else if (e instanceof APIError) {
console.log("APIError", e.message, e.code, e.fieldErrors); console.log("APIError", e.message, e.code, e.fieldErrors);
e.fieldErrors.forEach((fieldError) => { e.fieldErrors.forEach((fieldError) => {
setError(fieldError.field as any, { setError(fieldError.field as any, {
type: "manual", type: "manual",
message: fieldError.error, message: fieldError.error,
}); });
}); });
} else { } else {
console.log("General Error", e); console.log("General Error", e);
} }
}); });
}); });
const hasErrors = () => { const hasErrors = () => {
return Object.values(errors).some((error) => error); return Object.values(errors).some((error) => error);
}; };
return ( return (
<Wrapper> <Wrapper>
<LoginBox> <LoginBox>
<HeaderContainer> <HeaderContainer>
<Header>Create an account</Header> <Header>Create an account</Header>
{/* <SubHeader>We're so excited to see you again!</SubHeader> */} {/* <SubHeader>We're so excited to see you again!</SubHeader> */}
</HeaderContainer> </HeaderContainer>
<FormContainer onSubmit={onSubmit}> <FormContainer onSubmit={onSubmit}>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}> <InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.email}> <LabelWrapper error={!!errors.email}>
<InputLabel>Email</InputLabel> <InputLabel>Email</InputLabel>
{errors.email && ( {errors.email && (
<InputErrorText> <InputErrorText>
<> <>
<Divider>-</Divider> <Divider>-</Divider>
{errors.email.message} {errors.email.message}
</> </>
</InputErrorText> </InputErrorText>
)} )}
</LabelWrapper> </LabelWrapper>
<InputWrapper> <InputWrapper>
<Input <Input
type="email" type="email"
autoFocus autoFocus
{...register("email", { required: true })} {...register("email", { required: true })}
error={!!errors.email} error={!!errors.email}
/> />
</InputWrapper> </InputWrapper>
</InputContainer> </InputContainer>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}> <InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.username}> <LabelWrapper error={!!errors.username}>
<InputLabel>Username</InputLabel> <InputLabel>Username</InputLabel>
{errors.username && ( {errors.username && (
<InputErrorText> <InputErrorText>
<> <>
<Divider>-</Divider> <Divider>-</Divider>
{errors.username.message} {errors.username.message}
</> </>
</InputErrorText> </InputErrorText>
)} )}
</LabelWrapper> </LabelWrapper>
<InputWrapper> <InputWrapper>
<Input <Input
{...register("username", { required: true })} {...register("username", { required: true })}
error={!!errors.username} error={!!errors.username}
/> />
</InputWrapper> </InputWrapper>
</InputContainer> </InputContainer>
<InputContainer marginBottom={false}> <InputContainer marginBottom={false}>
<LabelWrapper error={!!errors.password}> <LabelWrapper error={!!errors.password}>
<InputLabel>Password</InputLabel> <InputLabel>Password</InputLabel>
{errors.password && ( {errors.password && (
<InputErrorText> <InputErrorText>
<> <>
<Divider>-</Divider> <Divider>-</Divider>
{errors.password.message} {errors.password.message}
</> </>
</InputErrorText> </InputErrorText>
)} )}
</LabelWrapper> </LabelWrapper>
<InputWrapper> <InputWrapper>
<Input <Input
type="password" type="password"
{...register("password", { required: true })} {...register("password", { required: true })}
error={!!errors.password} error={!!errors.password}
/> />
</InputWrapper> </InputWrapper>
</InputContainer> </InputContainer>
<InputContainer marginBottom={true}> <InputContainer marginBottom={true}>
<LabelWrapper error={!!errors.date_of_birth}> <LabelWrapper error={!!errors.date_of_birth}>
<InputLabel>Date of Birth</InputLabel> <InputLabel>Date of Birth</InputLabel>
{errors.date_of_birth && ( {errors.date_of_birth && (
<InputErrorText> <InputErrorText>
<> <>
<Divider>-</Divider> <Divider>-</Divider>
{errors.date_of_birth.message} {errors.date_of_birth.message}
</> </>
</InputErrorText> </InputErrorText>
)} )}
</LabelWrapper> </LabelWrapper>
<InputWrapper> <InputWrapper>
<DOBInput <DOBInput
onChange={(value) => setValue("date_of_birth", value)} onChange={(value) => setValue("date_of_birth", value)}
onErrorChange={(errors) => { onErrorChange={(errors) => {
const hasError = Object.values(errors).some((error) => error); const hasError = Object.values(errors).some((error) => error);
if (hasError) { if (hasError) {
// set to first error // set to first error
setError("date_of_birth", { setError("date_of_birth", {
type: "manual", type: "manual",
message: Object.values(errors).filter((x) => x)[0], message: Object.values(errors).filter((x) => x)[0],
}); });
} else clearErrors("date_of_birth"); } else clearErrors("date_of_birth");
}} }}
error={!!errors.date_of_birth} error={!!errors.date_of_birth}
/> />
</InputWrapper> </InputWrapper>
</InputContainer> </InputContainer>
<LoginButton variant="primary" type="submit" disabled={hasErrors()}> <LoginButton variant="primary" type="submit" disabled={hasErrors()}>
Create Account Create Account
</LoginButton> </LoginButton>
<LoginLink <LoginLink
onClick={() => { onClick={() => {
navigate("/login", { replace: true }); navigate("/login", { replace: true });
}} }}
type="button" type="button"
> >
Already have an account? Already have an account?
</LoginLink> </LoginLink>
</FormContainer> </FormContainer>
</LoginBox> </LoginBox>
</Wrapper> </Wrapper>
); );
} }
export default RegistrationPage; export default RegistrationPage;

View File

@ -4,13 +4,13 @@ import { useAppStore } from "../stores/AppStore";
import LoadingPage from "./LoadingPage"; import LoadingPage from "./LoadingPage";
function RootPage() { function RootPage() {
const app = useAppStore(); const app = useAppStore();
if (!app.ready) { if (!app.ready) {
return <LoadingPage />; return <LoadingPage />;
} }
return <Container>RootPage</Container>; return <Container>RootPage</Container>;
} }
export default observer(RootPage); export default observer(RootPage);

View File

@ -3,36 +3,36 @@ import { makeAutoObservable, observable, runInAction } from "mobx";
import ThemeStore from "./ThemeStore"; import ThemeStore from "./ThemeStore";
export default class AppStore { export default class AppStore {
theme: ThemeStore; theme: ThemeStore;
api: Client; api: Client;
@observable ready = false; @observable ready = false;
constructor() { constructor() {
this.theme = new ThemeStore(); this.theme = new ThemeStore();
this.api = new Client({ this.api = new Client({
rest: { rest: {
url: "https://canary.slowcord.understars.dev/api", url: "https://old.server.spacebar.chat/api",
}, },
}); });
this.api.on("debug", console.debug); this.api.on("debug", console.debug);
this.api.on("warn", console.warn); this.api.on("warn", console.warn);
this.api.on("error", console.error); this.api.on("error", console.error);
this.api.on("ready", this.onReady.bind(this)); this.api.on("ready", this.onReady.bind(this));
makeAutoObservable(this); makeAutoObservable(this);
} }
onReady() { onReady() {
runInAction(() => { runInAction(() => {
this.ready = true; this.ready = true;
}); });
} }
} }
export const appStore = new AppStore(); export const appStore = new AppStore();
export function useAppStore() { export function useAppStore() {
return appStore; return appStore;
} }

View File

@ -3,22 +3,22 @@ import type { Theme } from "../contexts/Theme";
import { ThemePresets } from "../contexts/Theme"; import { ThemePresets } from "../contexts/Theme";
export default class ThemeStore { export default class ThemeStore {
constructor() { constructor() {
makeAutoObservable(this); makeAutoObservable(this);
} }
@computed @computed
getVariables(): Theme { getVariables(): Theme {
return { return {
...ThemePresets["dark"], ...ThemePresets["dark"],
light: false, light: false,
}; };
} }
@computed @computed
computeVariables() { computeVariables() {
const variables = this.getVariables(); const variables = this.getVariables();
return variables as unknown as Theme; return variables as unknown as Theme;
} }
} }

View File

@ -1,22 +1,22 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es5",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"module": "esnext", "module": "esnext",
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"experimentalDecorators": true, "experimentalDecorators": true,
"emitDecoratorMetadata": true "emitDecoratorMetadata": true
}, },
"include": ["src"] "include": ["src"]
} }