diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ff1abe5..526c137 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "@types/node": "^16.11.45", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", - "axios": "^1.5.0", + "axios": "^0.27.2", "date-fns": "^2.29.1", "formik": "^2.2.9", "react": "^18.2.0", @@ -5417,13 +5417,12 @@ } }, "node_modules/axios": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", - "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" } }, "node_modules/axios/node_modules/form-data": { @@ -13498,11 +13497,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 78998e4..4c674c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,7 +18,7 @@ "@types/node": "^16.11.45", "@types/react": "^18.0.15", "@types/react-dom": "^18.0.6", - "axios": "^1.5.0", + "axios": "^0.27.2", "date-fns": "^2.29.1", "formik": "^2.2.9", "react": "^18.2.0", diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 3082277..7cf53d6 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -1,8 +1,16 @@ -import { BrowserRouter, Routes } from "react-router-dom"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; + +import ScrollToTop from "./components/ScrollToTop"; +import TopBar from "./components/TopBar"; +import Register from "./pages/Register"; const Router = () => ( - {} + + + + } /> + ); diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx new file mode 100644 index 0000000..d52828b --- /dev/null +++ b/frontend/src/components/AccountMenu.tsx @@ -0,0 +1,67 @@ +import axios from "axios"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import AccountCircle from "@mui/icons-material/AccountCircle"; +import { useQueryClient } from "@tanstack/react-query"; +import React, { useContext, useState } from "react"; +import { Link } from "react-router-dom"; + +import { AuthContext } from "src/AuthContext"; +import { useMutation } from "src/query"; + +const useLogout = () => { + const { setAuthenticated } = useContext(AuthContext); + const queryClient = useQueryClient(); + const { mutate: logout } = useMutation( + async () => await axios.delete("/sessions/"), + { + onSuccess: () => { + setAuthenticated(false); + queryClient.clear(); + }, + }, + ); + return logout; +}; + +const AccountMenu = () => { + const logout = useLogout(); + const [anchorEl, setAnchorEl] = useState(null); + + const onMenuOpen = (event: React.MouseEvent) => + setAnchorEl(event.currentTarget); + const onMenuClose = () => setAnchorEl(null); + + return ( + <> + + + + + + Change password + + + { + logout(); + onMenuClose(); + }} + > + Logout + + + + ); +}; + +export default AccountMenu; diff --git a/frontend/src/components/TopBar.tsx b/frontend/src/components/TopBar.tsx new file mode 100644 index 0000000..51e4482 --- /dev/null +++ b/frontend/src/components/TopBar.tsx @@ -0,0 +1,37 @@ +import AppBar from "@mui/material/AppBar"; +import Box from "@mui/material/Box"; +import Button from "@mui/material/Button"; +import Toolbar from "@mui/material/Toolbar"; +import React, { useContext } from "react"; +import { Link } from "react-router-dom"; + +import { AuthContext } from "src/AuthContext"; +import AccountMenu from "src/components/AccountMenu"; + +const sxToolbar = { + paddingLeft: "env(safe-area-inset-left)", + paddingRight: "env(safe-area-inset-right)", + paddingTop: "env(safe-area-inset-top)", +}; + +const TopBar = () => { + const { authenticated } = useContext(AuthContext); + + return ( + <> + + + + + + {authenticated ? : null} + + + + + ); +}; + +export default TopBar; diff --git a/frontend/src/pages/Register.tsx b/frontend/src/pages/Register.tsx new file mode 100644 index 0000000..d776085 --- /dev/null +++ b/frontend/src/pages/Register.tsx @@ -0,0 +1,99 @@ +import axios from "axios"; +import { Form, Formik, FormikHelpers } from "formik"; +import { useContext } from "react"; +import { useNavigate } from "react-router"; +import { useLocation } from "react-router-dom"; +import * as yup from "yup"; + +import EmailField from "../components/EmailField"; +import FormActions from "../components/FormActions"; +import LazyPasswordWithStrengthField from "../components/LazyPasswordWithStrengthField"; +import Title from "../components/Title"; +import { ToastContext } from "../ToastContext"; +import { useMutation } from "../query"; + +interface IForm { + email: string; + password: string; +} + +const useRegister = () => { + const navigate = useNavigate(); + const { addToast } = useContext(ToastContext); + const { mutateAsync: register } = useMutation( + async (data: IForm) => await axios.post("/members/", data), + ); + + return async (data: IForm, { setFieldError }: FormikHelpers) => { + try { + await register(data); + addToast("Registered", "success"); + navigate("/login/", { state: { email: data.email } }); + } catch (error: any) { + if ( + error.response?.status === 400 && + error.response?.data.code === "WEAK_PASSWORD" + ) { + setFieldError("password", "Password is too weak"); + } else { + addToast("Try again", "error"); + } + } + }; +}; + +const validationSchema = yup.object({ + email: yup.string().email("Email invalid").required("Required"), + password: yup.string().required("Required"), +}); + +const Register = () => { + const location = useLocation(); + const onSubmit = useRegister(); + + return ( + <> + + <Formik<IForm> + initialValues={{ + email: (location.state as any)?.email ?? "", + password: "", + }} + onSubmit={onSubmit} + validationSchema={validationSchema} + > + {({ dirty, isSubmitting, values }) => ( + <Form> + <EmailField fullWidth label="Email" name="email" required /> + <LazyPasswordWithStrengthField + autoComplete="new-password" + fullWidth + label="Password" + name="password" + required + /> + <FormActions + disabled={!dirty} + isSubmitting={isSubmitting} + label="Register" + links={[ + { + label: "Login", + to: "/login/", + state: { email: values.email }, + }, + { + label: "Reset password", + to: "/forgotten-password/", + state: { email: values.email }, + }, + ]} + /> + </Form> + )} + </Formik> + </> + ); +}; + +export default Register;