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;
|
Loading…
x
Reference in New Issue
Block a user