Navigation

The app we are building has only a few pages so we don't need much naviagion, if we did the Material-UI Drawer component would be a good choice. Instead we just need a header bar that looks good for logged out users and allows logged in users to navigate to the full todo list, and the change-password page, whilst allowing them to change the language and to logout.

First lets create a AccountMenu component that adds the functionality required for logged in users by adding the following to frontend/src/components/AccountMenu.tsx,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import Divider from "@material-ui/core/Divider";
import IconButton from "@material-ui/core/IconButton";
import Menu from "@material-ui/core/Menu";
import MenuItem from "@material-ui/core/MenuItem";
import AccountCircle from "@material-ui/icons/AccountCircle";
import axios from "axios";
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { useUID } from "react-uid";

import { AuthContext } from "src/AuthContext";
import { useMutation } from "src/query";

const AccountMenu = () => {
  const { t } = useTranslation();
  const { setAuthenticated } = useContext(AuthContext);
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
  const menuID = useUID();

  const onMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };

  const onMenuClose = () => {
    setAnchorEl(null);
  };

  const { mutate: logout } = useMutation(
    async () => {
      await axios.delete("/sessions/");
    },
    {
      onSuccess: () => {
        setAuthenticated(false);
        onMenuClose();
      },
    },
  );

  return (
    <>
      <IconButton
        aria-label="account of current user"
        aria-controls="menu-appbar"
        aria-haspopup="true"
        color="inherit"
        onClick={onMenuOpen}
      >
        <AccountCircle />
      </IconButton>
      <Menu
        anchorEl={anchorEl}
        anchorOrigin={{
          horizontal: "right",
          vertical: "top",
        }}
        id={menuID}
        keepMounted
        onClose={onMenuClose}
        open={Boolean(anchorEl)}
        transformOrigin={{
          horizontal: "right",
          vertical: "top",
        }}
      >
        <MenuItem component={Link} onClick={onMenuClose} to="/change-password/">
          {t("AccountMenu.changePassword")}
        </MenuItem>
        <Divider />
        <MenuItem onClick={() => logout()}>{t("AccountMenu.signout")}</MenuItem>
      </Menu>
    </>
  );
};

export default AccountMenu;

The use mutation on lines 29-39 actually logs the user out via a call to the backend. Everything else is boilerplate for a menu that is anchored to the account icon button. Note also on line 67 the usage of the Link component as this ensure the correct HTML semantic element is used.

We can then make use of this component in a TopBar component, by adding the following to frontend/src/components/TopBar.tsx,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import AppBar from "@material-ui/core/AppBar";
import Button from "@material-ui/core/Button";
import Toolbar from "@material-ui/core/Toolbar";
import React, { useContext } from "react";
import { Link } from "react-router-dom";
import styled from "styled-components";

import { AuthContext } from "src/AuthContext";
import AccountMenu from "src/components/AccountMenu";
import LanguageSelector from "src/components/LanguageSelector";

const SToolbar = styled(Toolbar)`
  padding-left: constant(safe-area-inset-left); /* iOS 11.0 */
  padding-left: env(safe-area-inset-left); /* iOS 11.2 */
  padding-right: constant(safe-area-inset-right); /* iOS 11.0 */
  padding-right: env(safe-area-inset-right); /* iOS 11.2 */
  padding-top: constant(safe-area-inset-top); /* iOS 11.0 */
  padding-top: env(safe-area-inset-top); /* iOS 11.2 */
`;

const SLeft = styled.div`
  flex-grow: 1;
`;

const TopBar = () => {
  const { authenticated } = useContext(AuthContext);

  return (
    <>
      <AppBar position="fixed">
        <SToolbar>
          <SLeft>
            <Button color="inherit" component={Link} to="/">
              {/* eslint-disable i18next/no-literal-string */}
              Tozo
              {/* eslint-enable */}
            </Button>
          </SLeft>
          <LanguageSelector />
          {authenticated ? <AccountMenu /> : null}
        </SToolbar>
      </AppBar>
      <SToolbar />
    </>
  );
};

export default TopBar;

The toolbar styling on lines 12-19 extends the TopBar such that it fills in any non-rectangular display space, most notably the iPhone notch. Read more here.

The additional Toolbar on line 43 after the AppBar ensures that the content that follows this component does not render underneath the AppBar.

Finnally the TopBar is used by adding it within the BrowserRouter in frontend/src/Router.tsx and translation keys are added to frontend/src/i18n.ts.