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/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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = () => (
 | 
			
		||||
  <BrowserRouter>
 | 
			
		||||
    <Routes>{}</Routes>
 | 
			
		||||
    <ScrollToTop />
 | 
			
		||||
    <TopBar />
 | 
			
		||||
    <Routes>
 | 
			
		||||
      <Route path="/register/" element={<Register />} />
 | 
			
		||||
    </Routes>
 | 
			
		||||
  </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