# todo-list(react-hooks)

# 组件

// TodoList.tsx

import React, { useState, useEffect } from "react";
import { Input, Icon } from "antd";
import moment from "moment";
import styles from "./styles.module.less";

const TODO_KEY = "_react_ts_test_todo_list";

interface ITodo {
  id: string;
  date: number;
  content: string;
  done: boolean;
}

const TodoItem = ({
  todo,
  onToggleStatus,
  onRemove,
}: {
  todo: ITodo;
  onToggleStatus: (todoId: string) => void;
  onRemove: (todoId: string) => void;
}) => {
  return (
    <li className={styles.item}>
      <p className={styles.content}>{todo.content}</p>
      <div className={styles.item_footer}>
        <span className={styles.date}>
          {moment(todo.date).format("YYYY-MM-DD hh:mm:ss")}
        </span>
        <Icon
          className={styles.btn}
          type="check-circle"
          title="toggle status"
          theme={todo.done ? "filled" : "outlined"}
          onClick={() => onToggleStatus(todo.id)}
        />
        <Icon
          className={styles.btn}
          type="delete"
          title="remove"
          onClick={() => onRemove(todo.id)}
        />
      </div>
    </li>
  );
};

const TodoCreator = ({ onSubmit }: { onSubmit: (todo: ITodo) => void }) => {
  const [content, setContent] = useState<string>("");

  return (
    <div className={styles.input_wrapper}>
      <Input
        type="text"
        name="new-todo"
        id="new-todo"
        autoComplete="false"
        placeholder="input todo, then hit 'Enther'"
        size="large"
        maxLength={30}
        className={styles.input}
        value={content}
        onChange={(e) => setContent(e.target.value)}
        onKeyUp={(e) => {
          if (content.trim() && e && (e.keyCode === 13 || e.which === 13)) {
            onSubmit({
              id: Math.random().toFixed(6),
              date: Date.now(),
              content,
              done: false,
            });
            setContent("");
          }
        }}
      />
    </div>
  );
};

const TodoList = () => {
  const todoListFromStorage = window.localStorage.getItem(TODO_KEY);
  const initTodoList: ITodo[] = todoListFromStorage
    ? JSON.parse(todoListFromStorage)
    : [];
  const [list, setList] = useState<ITodo[]>(initTodoList);

  useEffect(() => {
    const todoListToStorage = JSON.stringify(list);
    window.localStorage.setItem(TODO_KEY, todoListToStorage);
  }, [list]);

  return (
    <div className={styles.container}>
      <h1 className={styles.title}>Todo List</h1>
      <TodoCreator onSubmit={(todo) => setList([...list, todo])} />

      <ol className={styles.list}>
        {list
          .sort((a, b) => b.date - a.date)
          .map((item) => (
            <TodoItem
              key={item.id}
              todo={item}
              onToggleStatus={(id) =>
                setList(
                  list.map((v) => (v.id === id ? { ...v, done: !v.done } : v))
                )
              }
              onRemove={(id) => setList(list.filter((v) => v.id !== id))}
            />
          ))}
      </ol>
    </div>
  );
};

export default TodoList;

# 样式

// styles.module.less

@spacing: 20px;
@font-size: 20px;

.container {
  margin: 30px auto;
  width: 576px;
  max-width: 100%;
  user-select: none;
}

.title {
  font-size: 1.5 * @font-size;
  font-weight: bold;
  text-align: center;
  color: @theme-color;
}

.input_wrapper {
  margin: @spacing;
}

.list {
  list-style: none;
  padding-left: 0;
  margin: @spacing;
}

.item {
  padding: @spacing;
  margin: 2 * @spacing 0;
  border-radius: 5px;
  box-shadow: 0 0 @spacing 0 #ddd;
}

.content {
  margin-right: auto;
  font-size: @font-size;
  word-break: break-all;
}

.item_footer {
  display: flex;
  align-items: center;
  padding-top: @spacing;
  border-top: 1px solid #ddd;
}

.date {
  margin-right: auto;
  font-weight: bold;
  color: #666;
}

.btn {
  margin-left: @spacing;
  color: @theme-color;
  font-size: @font-size;
}
Last Updated: 12/2/2020, 8:12:23 PM