Merge backend + frontend #1
							
								
								
									
										18
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -23,7 +23,7 @@
 | 
				
			|||||||
        "@types/node": "^16.11.45",
 | 
					        "@types/node": "^16.11.45",
 | 
				
			||||||
        "@types/react": "^18.0.15",
 | 
					        "@types/react": "^18.0.15",
 | 
				
			||||||
        "@types/react-dom": "^18.0.6",
 | 
					        "@types/react-dom": "^18.0.6",
 | 
				
			||||||
        "axios": "^1.5.0",
 | 
					        "axios": "^0.27.2",
 | 
				
			||||||
        "date-fns": "^2.29.1",
 | 
					        "date-fns": "^2.29.1",
 | 
				
			||||||
        "formik": "^2.2.9",
 | 
					        "formik": "^2.2.9",
 | 
				
			||||||
        "react": "^18.2.0",
 | 
					        "react": "^18.2.0",
 | 
				
			||||||
@@ -5417,13 +5417,12 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/axios": {
 | 
					    "node_modules/axios": {
 | 
				
			||||||
      "version": "1.5.0",
 | 
					      "version": "0.27.2",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
 | 
				
			||||||
      "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
 | 
					      "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "follow-redirects": "^1.15.0",
 | 
					        "follow-redirects": "^1.14.9",
 | 
				
			||||||
        "form-data": "^4.0.0",
 | 
					        "form-data": "^4.0.0"
 | 
				
			||||||
        "proxy-from-env": "^1.1.0"
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/axios/node_modules/form-data": {
 | 
					    "node_modules/axios/node_modules/form-data": {
 | 
				
			||||||
@@ -13498,11 +13497,6 @@
 | 
				
			|||||||
        "node": ">= 0.10"
 | 
					        "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": {
 | 
					    "node_modules/psl": {
 | 
				
			||||||
      "version": "1.9.0",
 | 
					      "version": "1.9.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@
 | 
				
			|||||||
    "@types/node": "^16.11.45",
 | 
					    "@types/node": "^16.11.45",
 | 
				
			||||||
    "@types/react": "^18.0.15",
 | 
					    "@types/react": "^18.0.15",
 | 
				
			||||||
    "@types/react-dom": "^18.0.6",
 | 
					    "@types/react-dom": "^18.0.6",
 | 
				
			||||||
    "axios": "^1.5.0",
 | 
					    "axios": "^0.27.2",
 | 
				
			||||||
    "date-fns": "^2.29.1",
 | 
					    "date-fns": "^2.29.1",
 | 
				
			||||||
    "formik": "^2.2.9",
 | 
					    "formik": "^2.2.9",
 | 
				
			||||||
    "react": "^18.2.0",
 | 
					    "react": "^18.2.0",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 = () => (
 | 
					const Router = () => (
 | 
				
			||||||
  <BrowserRouter>
 | 
					  <BrowserRouter>
 | 
				
			||||||
    <Routes>{}</Routes>
 | 
					    <ScrollToTop />
 | 
				
			||||||
 | 
					    <TopBar />
 | 
				
			||||||
 | 
					    <Routes>
 | 
				
			||||||
 | 
					      <Route path="/register/" element={<Register />} />
 | 
				
			||||||
 | 
					    </Routes>
 | 
				
			||||||
  </BrowserRouter>
 | 
					  </BrowserRouter>
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										67
									
								
								frontend/src/components/AccountMenu.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								frontend/src/components/AccountMenu.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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 | HTMLElement>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const onMenuOpen = (event: React.MouseEvent<HTMLElement>) =>
 | 
				
			||||||
 | 
					    setAnchorEl(event.currentTarget);
 | 
				
			||||||
 | 
					  const onMenuClose = () => setAnchorEl(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <IconButton color="inherit" onClick={onMenuOpen}>
 | 
				
			||||||
 | 
					        <AccountCircle />
 | 
				
			||||||
 | 
					      </IconButton>
 | 
				
			||||||
 | 
					      <Menu
 | 
				
			||||||
 | 
					        anchorEl={anchorEl}
 | 
				
			||||||
 | 
					        anchorOrigin={{ horizontal: "right", vertical: "top" }}
 | 
				
			||||||
 | 
					        keepMounted
 | 
				
			||||||
 | 
					        onClose={onMenuClose}
 | 
				
			||||||
 | 
					        open={Boolean(anchorEl)}
 | 
				
			||||||
 | 
					        transformOrigin={{ horizontal: "right", vertical: "top" }}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <MenuItem component={Link} onClick={onMenuClose} to="/change-password/">
 | 
				
			||||||
 | 
					          Change password
 | 
				
			||||||
 | 
					        </MenuItem>
 | 
				
			||||||
 | 
					        <Divider />
 | 
				
			||||||
 | 
					        <MenuItem
 | 
				
			||||||
 | 
					          onClick={() => {
 | 
				
			||||||
 | 
					            logout();
 | 
				
			||||||
 | 
					            onMenuClose();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          Logout
 | 
				
			||||||
 | 
					        </MenuItem>
 | 
				
			||||||
 | 
					      </Menu>
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default AccountMenu;
 | 
				
			||||||
							
								
								
									
										37
									
								
								frontend/src/components/TopBar.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								frontend/src/components/TopBar.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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 (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <AppBar position="fixed">
 | 
				
			||||||
 | 
					        <Toolbar sx={sxToolbar}>
 | 
				
			||||||
 | 
					          <Box sx={{ flexGrow: 1 }}>
 | 
				
			||||||
 | 
					            <Button color="inherit" component={Link} to="/">
 | 
				
			||||||
 | 
					              Todo
 | 
				
			||||||
 | 
					            </Button>
 | 
				
			||||||
 | 
					          </Box>
 | 
				
			||||||
 | 
					          {authenticated ? <AccountMenu /> : null}
 | 
				
			||||||
 | 
					        </Toolbar>
 | 
				
			||||||
 | 
					      </AppBar>
 | 
				
			||||||
 | 
					      <Toolbar sx={{ ...sxToolbar, marginBottom: 2 }} />
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default TopBar;
 | 
				
			||||||
							
								
								
									
										99
									
								
								frontend/src/pages/Register.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								frontend/src/pages/Register.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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<IForm>) => {
 | 
				
			||||||
 | 
					    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 (
 | 
				
			||||||
 | 
					    <>
 | 
				
			||||||
 | 
					      <Title title="Register" />
 | 
				
			||||||
 | 
					      <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;
 | 
				
			||||||
		Reference in New Issue
	
	Block a user