Feat(frontend): navigation and registration page

This commit is contained in:
2023-09-04 16:29:32 -04:00
parent 1b9d1ee7d5
commit bf31eeb652
6 changed files with 220 additions and 15 deletions

View File

@@ -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>
);

View 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;

View 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;

View 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;