View todos

The todos page is effectively the user's homepage, it needs to show the user's todos, and provide actions to create new ones and edit the existing. The action to create a todo is a good use case for a floating action button, as it should have an unambiguous meaning. Whereas the editing action is best triggered by clicking each individual todo.

To provide a better user experience we can show skeleton todos whilst we wait for the todo API to respond. I've chosen to show 3 skeleton todos to give an indication that there will be a few.

Note also the due date is formatted according to the user's chosen language.

The following code to do all this should be placed in frontend/src/pages/Todos.tsx,

import Checkbox from "@material-ui/core/Checkbox";
import Fab from "@material-ui/core/Fab";
import List from "@material-ui/core/List";
import ListItem from "@material-ui/core/ListItem";
import ListItemIcon from "@material-ui/core/ListItemIcon";
import ListItemText from "@material-ui/core/ListItemText";
import AddIcon from "@material-ui/icons/Add";
import Skeleton from "@material-ui/lab/Skeleton";
import axios from "axios";
import { format } from "date-fns";
import { enGB, de } from "date-fns/locale";
import React from "react";
import { useTranslation } from "react-i18next";
import { Link, Redirect } from "react-router-dom";
import styled from "styled-components";

import { Todo } from "src/models";
import { useQuery } from "src/query";

const SFab: typeof Fab = styled(Fab)`
  bottom: ${(props) => props.theme.spacing(2)}px;
  position: fixed;
  right: ${(props) => props.theme.spacing(2)}px;
`;

const Todos = () => {
  const { data: todos } = useQuery<Todo[]>("todos", async () => {
    const response = await axios.get("/todos/");
    return response.data.todos.map((json: any) => new Todo(json));
  });
  const { i18n } = useTranslation();

  const locale = i18n.language === "en" ? enGB : de;

  if (todos?.length === 0) {
    return <Redirect to="/todos/new/" />;
  } else {
    return (
      <>
        <List>
          {todos?.map((todo: Todo) => {
            const secondary = todo.due
              ? format(todo.due, "P", { locale })
              : undefined;
            return (
              <ListItem
                key={todo.id}
                button={true}
                component={Link}
                to={`/todos/${todo.id}/`}
              >
                <ListItemIcon>
                  <Checkbox
                    checked={todo.complete}
                    disabled={true}
                    disableRipple={true}
                    edge="start"
                    tabIndex={-1}
                  />
                </ListItemIcon>
                <ListItemText primary={todo.task} secondary={secondary} />
              </ListItem>
            );
          }) ??
            [1, 2, 3].map((id) => (
              <ListItem key={id}>
                <ListItemIcon>
                  <Checkbox
                    disabled={true}
                    disableRipple={true}
                    edge="start"
                    tabIndex={-1}
                  />
                </ListItemIcon>
                <ListItemText
                  primary={<Skeleton />}
                  secondary={<Skeleton width="200px" />}
                />
              </ListItem>
            ))}
        </List>
        <SFab component={Link} to="/todos/new/">
          <AddIcon />
        </SFab>
      </>
    );
  }
};

export default Todos;

Then we can add the page to the routing (twice for the two relevant paths) by adding the following to frontend/src/Router.tsx,

import PrivateRoute from "src/components/PrivateRoute";
import Todos from "src/pages/Todos";
...

const Router = () => (
  <BrowserRouter>
    ...
    <PrivateRoute exact={true} path="/">
      <Todos />
    </PrivateRoute>
    <PrivateRoute exact={true} path="/todos/">
      <Todos />
    </PrivateRoute>
  </BrowserRouter>
);