From ba85ceb283240174ebd951dc268d8312e78b23fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Huy=20V=C5=A9?= Date: Sun, 8 May 2022 23:27:03 +0700 Subject: [PATCH 1/2] First commit --- package.json | 7 + src/App.css | 125 ---------- src/App.scss | 46 ++++ src/App.tsx | 9 +- src/ToDoPage.tsx | 107 -------- src/components/button/Button.tsx | 35 +++ .../button/__test__/Button.test.tsx | 55 +++++ .../__snapshots__/Button.test.tsx.snap | 10 + src/components/button/index.ts | 1 + src/components/button/styles.scss | 44 ++++ src/components/checkbox/Checkbox.tsx | 32 +++ .../checkbox/__test__/Checkbox.test.tsx | 55 +++++ .../__snapshots__/Checkbox.test.tsx.snap | 12 + src/components/checkbox/index.ts | 1 + src/components/checkbox/styles.scss | 37 +++ src/components/input/Input.tsx | 33 +++ src/components/input/__test__/Input.test.tsx | 53 ++++ .../__snapshots__/Input.test.tsx.snap | 13 + src/components/input/index.ts | 1 + src/components/input/styles.scss | 4 + .../todoItemList/TodoItem/TodoItem.tsx | 111 +++++++++ .../TodoItem/__test__/TodoItem.test.tsx | 81 ++++++ .../__snapshots__/TodoItem.test.tsx.snap | 64 +++++ .../todoItemList/TodoItem/styles.scss | 51 ++++ src/components/todoItemList/TodoItemList.tsx | 26 ++ src/components/todoItemList/index.ts | 2 + src/components/todoItemList/styles.scss | 5 + src/components/todoTab/TodoTab.tsx | 54 ++++ .../todoTab/__test__/TodoTab.test.tsx | 91 +++++++ .../__snapshots__/TodoTab.test.tsx.snap | 39 +++ src/components/todoTab/data.ts | 22 ++ src/components/todoTab/index.ts | 1 + src/components/todoTab/styles.scss | 8 + src/containers/ToDoPage.tsx | 103 ++++++++ src/containers/__test__/ToDoPage.test.tsx | 38 +++ .../__snapshots__/ToDoPage.test.tsx.snap | 66 +++++ src/containers/index.ts | 1 + src/hooks/index.ts | 2 + src/hooks/useOnClickOutside.tsx | 30 +++ src/hooks/useStateReducer.tsx | 23 ++ src/index.css | 13 - src/index.tsx | 12 +- src/models/eventListener.ts | 4 + src/models/index.ts | 5 + src/models/inputType.ts | 4 + src/models/localStorage.ts | 1 + src/models/todo.ts | 14 +- src/service/api-frontend.ts | 44 ++-- src/service/api-fullstack.ts | 31 ++- src/service/index.ts | 12 +- src/service/types.ts | 8 +- src/setupTests.ts | 2 + src/store/action-handlers.ts | 81 ++++++ src/store/action-types.ts | 7 + src/store/actions.ts | 111 +++------ src/store/reducer.ts | 97 +++++--- src/styles/index.scss | 15 ++ src/styles/variables.scss | 6 + src/utils/axios.ts | 18 +- src/utils/index.ts | 20 +- src/utils/storage.ts | 27 ++ src/utils/testUtils.ts | 13 + tsconfig.json | 13 +- yarn.lock | 233 +++++++++++++++++- 64 files changed, 1846 insertions(+), 443 deletions(-) delete mode 100644 src/App.css create mode 100644 src/App.scss delete mode 100644 src/ToDoPage.tsx create mode 100644 src/components/button/Button.tsx create mode 100644 src/components/button/__test__/Button.test.tsx create mode 100644 src/components/button/__test__/__snapshots__/Button.test.tsx.snap create mode 100644 src/components/button/index.ts create mode 100644 src/components/button/styles.scss create mode 100644 src/components/checkbox/Checkbox.tsx create mode 100644 src/components/checkbox/__test__/Checkbox.test.tsx create mode 100644 src/components/checkbox/__test__/__snapshots__/Checkbox.test.tsx.snap create mode 100644 src/components/checkbox/index.ts create mode 100644 src/components/checkbox/styles.scss create mode 100644 src/components/input/Input.tsx create mode 100644 src/components/input/__test__/Input.test.tsx create mode 100644 src/components/input/__test__/__snapshots__/Input.test.tsx.snap create mode 100644 src/components/input/index.ts create mode 100644 src/components/input/styles.scss create mode 100644 src/components/todoItemList/TodoItem/TodoItem.tsx create mode 100644 src/components/todoItemList/TodoItem/__test__/TodoItem.test.tsx create mode 100644 src/components/todoItemList/TodoItem/__test__/__snapshots__/TodoItem.test.tsx.snap create mode 100644 src/components/todoItemList/TodoItem/styles.scss create mode 100644 src/components/todoItemList/TodoItemList.tsx create mode 100644 src/components/todoItemList/index.ts create mode 100644 src/components/todoItemList/styles.scss create mode 100644 src/components/todoTab/TodoTab.tsx create mode 100644 src/components/todoTab/__test__/TodoTab.test.tsx create mode 100644 src/components/todoTab/__test__/__snapshots__/TodoTab.test.tsx.snap create mode 100644 src/components/todoTab/data.ts create mode 100644 src/components/todoTab/index.ts create mode 100644 src/components/todoTab/styles.scss create mode 100644 src/containers/ToDoPage.tsx create mode 100644 src/containers/__test__/ToDoPage.test.tsx create mode 100644 src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap create mode 100644 src/containers/index.ts create mode 100644 src/hooks/index.ts create mode 100644 src/hooks/useOnClickOutside.tsx create mode 100644 src/hooks/useStateReducer.tsx delete mode 100644 src/index.css create mode 100644 src/models/eventListener.ts create mode 100644 src/models/index.ts create mode 100644 src/models/inputType.ts create mode 100644 src/models/localStorage.ts create mode 100644 src/setupTests.ts create mode 100644 src/store/action-handlers.ts create mode 100644 src/store/action-types.ts create mode 100644 src/styles/index.scss create mode 100644 src/styles/variables.scss create mode 100644 src/utils/storage.ts create mode 100644 src/utils/testUtils.ts diff --git a/package.json b/package.json index 699d92678..842c3f065 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,10 @@ "cross-env": "^7.0.2", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-icons": "^4.3.1", "react-scripts": "4.0.3", + "react-toastify": "^9.0.1", + "sass": "^1.51.0", "shortid": "^2.2.15", "typescript": "4.5.3" }, @@ -39,6 +42,10 @@ ] }, "devDependencies": { + "@testing-library/dom": "^8.11.1", + "@testing-library/jest-dom": "^5.16.1", + "@testing-library/react": "12.1.2", + "@testing-library/user-event": "^13.5.0", "@types/shortid": "^0.0.29" } } diff --git a/src/App.css b/src/App.css deleted file mode 100644 index cc74a3fea..000000000 --- a/src/App.css +++ /dev/null @@ -1,125 +0,0 @@ -.App { - text-align: center; - display: flex; - justify-content: center; -} - -button { - outline: none; - border: none; - box-shadow: 2px 0 2px currentColor; - border-radius: 4px; - min-height: 32px; - min-width: 80px; - padding: 4px 8px; -} - -button:hover { - opacity: 0.85; -} - -input[type="checkbox"] { - width: 24px; - height: 24px; - box-shadow: none; - border: none; - outline: none; -} - -input[type="checkbox"]:focus { - box-shadow: none; - border: none; - outline: none; -} - -input { - min-height: 36px; - border: none; - outline: none; - padding: 0 12px; - box-shadow: 2px 0 4px rgba(0,0,0, 0.2); - border-radius: 4px; -} - -input:focus { - box-shadow: 1px 0 9px rgba(0,0,0, 0.25); -} - -.ToDo__container { - border: 1px solid rgba(0,0,0, 0.13); - border-radius: 8px; - width: 500px; - margin-top: 5rem; - padding: 24px; - box-shadow: 2px 2px 1px rgba(0,0,0, 0.09), - 3px 2px 3px rgba(0,0,0, 0.05); -} - -.Todo__creation { - display: flex; -} - -.Todo__input { - flex: 1 1; -} - -.ToDo__list { - display: flex; - flex-direction: column; - margin-top: 1.5rem; -} - -.ToDo__item { - display: flex; - align-items: center; - justify-content: space-between; -} - -.ToDo__item > span { - flex: 1 1; - text-align: left; - margin-left: 8px; -} - -.Todo__content { - flex: 1 1; -} - -.Todo__delete { - outline: none; - border: none; - width: 32px; - height: 32px; - min-width: auto; - min-height: auto; - box-shadow: none; - border-radius: 50%; - display: flex; - justify-content: center; - align-items: center; - cursor: pointer; -} - -.Todo__action, .Todo__delete { - width: 24px; - height: 24px; - flex-shrink: 0; -} - -.Todo__toolbar { - display: flex; - justify-content: space-between; - margin-top: 24px; -} - -.Todo__tabs { - display: flex; - justify-content: center; -} - -.Todo__tabs > *:not(:last-child) { - margin-right: 8px; -} - -.Action__btn { -} \ No newline at end of file diff --git a/src/App.scss b/src/App.scss new file mode 100644 index 000000000..6a4cf769b --- /dev/null +++ b/src/App.scss @@ -0,0 +1,46 @@ +.App { + text-align: center; + display: flex; + justify-content: center; +} + +input { + border: none; + outline: none; + padding: 0 12px; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +input:focus { + box-shadow: 1px 0 9px rgba(0, 0, 0, 0.25); +} + +.ToDo__container { + border: 1px solid rgba(0, 0, 0, 0.13); + border-radius: 8px; + width: 500px; + margin-top: 5rem; + padding: 24px; + box-shadow: 2px 2px 1px rgba(0, 0, 0, 0.09), 3px 2px 3px rgba(0, 0, 0, 0.05); +} + +.Todo__creation { + display: flex; +} + +.Todo__content { + flex: 1 1; +} + +.Todo__action { + width: 24px; + height: 24px; + flex-shrink: 0; +} + +.Todo__toolbar { + display: flex; + justify-content: space-between; + margin-top: 24px; +} diff --git a/src/App.tsx b/src/App.tsx index 2395f3d30..126fef93b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,8 +1,7 @@ -import React from 'react'; - -import ToDoPage from './ToDoPage'; - -import './App.css'; +import { ToDoPage } from "containers"; +import React from "react"; +import "react-toastify/dist/ReactToastify.css"; +import "./App.scss"; function App() { return ( diff --git a/src/ToDoPage.tsx b/src/ToDoPage.tsx deleted file mode 100644 index 1909718d0..000000000 --- a/src/ToDoPage.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import React, {useEffect, useReducer, useRef, useState} from 'react'; - -import reducer, {initialState} from './store/reducer'; -import { - setTodos, - createTodo, - toggleAllTodos, - deleteAllTodos, - updateTodoStatus -} from './store/actions'; -import Service from './service'; -import {TodoStatus} from './models/todo'; - -type EnhanceTodoStatus = TodoStatus | 'ALL'; - - -const ToDoPage = () => { - const [{todos}, dispatch] = useReducer(reducer, initialState); - const [showing, setShowing] = useState('ALL'); - const inputRef = useRef(null); - - useEffect(()=>{ - (async ()=>{ - const resp = await Service.getTodos(); - - dispatch(setTodos(resp || [])); - })() - }, []) - - const onCreateTodo = async (e: React.KeyboardEvent) => { - if (e.key === 'Enter' ) { - const resp = await Service.createTodo(inputRef.current.value); - dispatch(createTodo(resp)); - } - } - - const onUpdateTodoStatus = (e: React.ChangeEvent, todoId: any) => { - dispatch(updateTodoStatus(todoId, e.target.checked)) - } - - const onToggleAllTodo = (e: React.ChangeEvent) => { - dispatch(toggleAllTodos(e.target.checked)) - } - - const onDeleteAllTodo = () => { - dispatch(deleteAllTodos()); - } - - - return ( -
-
- -
-
- { - todos.map((todo, index) => { - return ( -
- onUpdateTodoStatus(e, index)} - /> - {todo.content} - -
- ); - }) - } -
-
- {todos.length > 0 ? - :
- } -
- - - -
- -
-
- ); -}; - -export default ToDoPage; \ No newline at end of file diff --git a/src/components/button/Button.tsx b/src/components/button/Button.tsx new file mode 100644 index 000000000..ba046150b --- /dev/null +++ b/src/components/button/Button.tsx @@ -0,0 +1,35 @@ +import React, { + ButtonHTMLAttributes, + FC, + memo, + MouseEventHandler, +} from "react"; +import "./styles.scss"; + +export interface IButtonProps extends ButtonHTMLAttributes { + className?: string; + disabled?: boolean; + onClick: MouseEventHandler | undefined; +} + +const Button: FC = ({ + className, + children, + disabled, + onClick, + ...props +}) => { + return ( + + ); +}; + +export default memo(Button); diff --git a/src/components/button/__test__/Button.test.tsx b/src/components/button/__test__/Button.test.tsx new file mode 100644 index 000000000..c2ca1d860 --- /dev/null +++ b/src/components/button/__test__/Button.test.tsx @@ -0,0 +1,55 @@ +import { render, RenderResult } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { getElementByTestId } from "utils/testUtils"; +import Button, { IButtonProps } from "../Button"; + +const onClickAction = jest.fn(); + +const renderComponent = ({ ...props }: IButtonProps): RenderResult => { + return render(
+`; diff --git a/src/components/button/index.ts b/src/components/button/index.ts new file mode 100644 index 000000000..1fdf4382e --- /dev/null +++ b/src/components/button/index.ts @@ -0,0 +1 @@ +export { default as Button } from "./Button"; diff --git a/src/components/button/styles.scss b/src/components/button/styles.scss new file mode 100644 index 000000000..551e6f1c9 --- /dev/null +++ b/src/components/button/styles.scss @@ -0,0 +1,44 @@ +.btn-todo { + outline: none; + border: none; + font-weight: 700; + border-radius: 4px; + height: 40px; + min-width: 80px; + padding: 4px 8px; + cursor: pointer; + transition: all 0.2s ease-in-out; + + &:disabled { + pointer-events: none; + } + + &:hover { + opacity: 0.85; + background-color: var(--color-primary); + color: #fff; + } + + &--main { + border: 1px solid var(--color-primary-light); + color: var(--color-primary); + background-color: #f1f5f9; + } + + &--selected { + background-color: var(--color-primary); + color: #fff; + } + + &--danger { + border: 1px solid var(--color-secondary); + background-color: var(--color-secondary); + color: #fff; + + &:hover { + opacity: 0.85; + background-color: var(--color-secondary); + color: #fff; + } + } +} diff --git a/src/components/checkbox/Checkbox.tsx b/src/components/checkbox/Checkbox.tsx new file mode 100644 index 000000000..f7bb4fa2a --- /dev/null +++ b/src/components/checkbox/Checkbox.tsx @@ -0,0 +1,32 @@ +import { InputType } from "models"; +import React, { FC, memo } from "react"; +import "./styles.scss"; + +export interface ICheckboxProps { + checked?: boolean; + id?: string; + className?: string; + onChange: (e: React.ChangeEvent) => void; +} + +const Checkbox: FC = ({ + checked, + onChange, + className, + id, + ...props +}) => { + return ( + + ); +}; + +export default memo(Checkbox); diff --git a/src/components/checkbox/__test__/Checkbox.test.tsx b/src/components/checkbox/__test__/Checkbox.test.tsx new file mode 100644 index 000000000..5e93b8c9c --- /dev/null +++ b/src/components/checkbox/__test__/Checkbox.test.tsx @@ -0,0 +1,55 @@ +import { render, RenderResult } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { getElementByTestId } from "utils/testUtils"; +import Checkbox, { ICheckboxProps } from "../Checkbox"; + +const onChangeAction = jest.fn(); + +const renderComponent = ({ ...props }: ICheckboxProps): RenderResult => { + return render(); +}; + +describe(" rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent({ + onChange: onChangeAction, + }); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render correcly", () => { + renderComponent({ + onChange: onChangeAction, + }); + const element = getElementByTestId("checkbox"); + expect(element).toBeInTheDocument(); + }); + + it("should render a checkbox with the default class", () => { + renderComponent({ + onChange: onChangeAction, + }); + const element = getElementByTestId("checkbox"); + expect(element).toHaveClass("cbx-todo"); + }); +}); + +describe(" interacting", () => { + it("should render a checkbox clickable", () => { + renderComponent({ + onChange: onChangeAction, + }); + const element = getElementByTestId("checkbox"); + expect(element).not.toBeDisabled(); + }); + + it("should handle onChange when clicked", () => { + renderComponent({ + onChange: onChangeAction, + }); + const element = getElementByTestId("checkbox"); + userEvent.click(element); + expect(onChangeAction).toHaveBeenCalled(); + }); +}); diff --git a/src/components/checkbox/__test__/__snapshots__/Checkbox.test.tsx.snap b/src/components/checkbox/__test__/__snapshots__/Checkbox.test.tsx.snap new file mode 100644 index 000000000..e698f31f3 --- /dev/null +++ b/src/components/checkbox/__test__/__snapshots__/Checkbox.test.tsx.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` rendering should match snapshot 1`] = ` +
+ +
+`; diff --git a/src/components/checkbox/index.ts b/src/components/checkbox/index.ts new file mode 100644 index 000000000..b1de1e0f8 --- /dev/null +++ b/src/components/checkbox/index.ts @@ -0,0 +1 @@ +export { default as Checkbox } from "./Checkbox"; diff --git a/src/components/checkbox/styles.scss b/src/components/checkbox/styles.scss new file mode 100644 index 000000000..db4775eb9 --- /dev/null +++ b/src/components/checkbox/styles.scss @@ -0,0 +1,37 @@ +.cbx-todo { + position: relative; + width: 30px; + height: 30px; + color: #363839; + border: 1px solid #bdc1c6; + border-radius: 4px; + appearance: none; + outline: 0; + cursor: pointer; + transition: background 175ms cubic-bezier(0.1, 0.1, 0.25, 1); + &::before { + position: absolute; + content: ""; + display: block; + top: 3px; + left: 10px; + width: 8px; + height: 14px; + border-style: solid; + border-color: #fff; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + opacity: 0; + } + &:checked { + color: #fff; + border-color: var(--color-primary-dark); + background: var(--color-primary-dark); + &::before { + opacity: 1; + } + ~ label::before { + clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); + } + } +} diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx new file mode 100644 index 000000000..f3cd903f9 --- /dev/null +++ b/src/components/input/Input.tsx @@ -0,0 +1,33 @@ +import React, { forwardRef, InputHTMLAttributes, memo } from "react"; +import { InputType } from "../../models"; +import "./styles.scss"; + +export interface IInputProps extends InputHTMLAttributes { + placeholder?: string; + className?: string; + disabled?: boolean; + id?: string; + name: string; + onKeyDown?: (e: React.KeyboardEvent) => Promise; +} + +const Input = forwardRef( + ({ placeholder, disabled, onKeyDown, className, id, name, ...props }, ref) => { + return ( + + ); + } +); + +export default memo(Input); diff --git a/src/components/input/__test__/Input.test.tsx b/src/components/input/__test__/Input.test.tsx new file mode 100644 index 000000000..d839c84e8 --- /dev/null +++ b/src/components/input/__test__/Input.test.tsx @@ -0,0 +1,53 @@ +import { fireEvent, render, RenderResult } from "@testing-library/react"; +import React from "react"; +import { getElementByTestId } from "utils/testUtils"; +import Input, { IInputProps } from "../Input"; + +const onKeyDownAction = jest.fn(); + +const renderComponent = ({ ...props }: IInputProps): RenderResult => { + return render(); +}; + +describe(" rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent({ name: "todo-input" }); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render correcly", () => { + renderComponent({ name: "todo-input" }); + const element = getElementByTestId("input"); + expect(element).toBeInTheDocument(); + }); + + it("should render a input with the default class", () => { + renderComponent({ name: "todo-input" }); + const element = getElementByTestId("input"); + expect(element).toHaveClass("input-todo"); + }); +}); + +describe(" interacting", () => { + it("should render a input clickable", () => { + renderComponent({ name: "todo-input" }); + const element = getElementByTestId("input"); + expect(element).not.toBeDisabled(); + }); + + it("should handle onKeydown", () => { + renderComponent({ + name: "todo-input", + onKeyDown: onKeyDownAction, + }); + const element = getElementByTestId("input"); + + fireEvent.keyDown(element, { + key: "Enter", + code: "Enter", + charCode: 13, + }); + + expect(onKeyDownAction).toHaveBeenCalled(); + }); +}); diff --git a/src/components/input/__test__/__snapshots__/Input.test.tsx.snap b/src/components/input/__test__/__snapshots__/Input.test.tsx.snap new file mode 100644 index 000000000..a36086dc3 --- /dev/null +++ b/src/components/input/__test__/__snapshots__/Input.test.tsx.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` rendering should match snapshot 1`] = ` +
+ +
+`; diff --git a/src/components/input/index.ts b/src/components/input/index.ts new file mode 100644 index 000000000..a5f313c2e --- /dev/null +++ b/src/components/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input"; diff --git a/src/components/input/styles.scss b/src/components/input/styles.scss new file mode 100644 index 000000000..f012c127b --- /dev/null +++ b/src/components/input/styles.scss @@ -0,0 +1,4 @@ +.input-todo { + flex: 1 1; + min-height: 36px; +} diff --git a/src/components/todoItemList/TodoItem/TodoItem.tsx b/src/components/todoItemList/TodoItem/TodoItem.tsx new file mode 100644 index 000000000..df3a3c091 --- /dev/null +++ b/src/components/todoItemList/TodoItem/TodoItem.tsx @@ -0,0 +1,111 @@ +import { Button } from "components/button"; +import { Checkbox } from "components/checkbox"; +import { Input } from "components/input"; +import { useOnClickOutside } from "hooks"; +import { InputType, Todo } from "models"; +import React, { Dispatch, FC, memo, useRef, useState } from "react"; +import { IoTrashBin } from "react-icons/io5"; +import { + deleteTodo, + updateTodoContent, + updateTodoStatus, +} from "store/action-handlers"; +import { AppActions } from "store/actions"; +import { getTodoStatus, isTodoCompleted } from "utils"; +import "./styles.scss"; + +export interface ITodoItemProps extends Todo { + dispatch: Dispatch; + className?: string; +} + +const TodoItem: FC = ({ className, dispatch, ...props }) => { + const { id, content, status } = props; + + const inputRef = useRef(null); + const [isEdit, setIsEdit] = useState(false); + + /** Event handlers --- start */ + + const handleUpdateTodoStatus = ( + e: React.ChangeEvent + ): void => { + dispatch(updateTodoStatus(id, getTodoStatus(e.target.checked))); + }; + + const handleDeleteTodoItem = (): void => { + dispatch(deleteTodo(id)); + }; + + const handleDoubleClickToEdit = () => { + setIsEdit(true); + }; + + const handleUpdateTodo = async ( + e: React.KeyboardEvent + ): Promise => { + if (e.key === "Enter" && inputRef.current) { + const { value } = inputRef.current; + if (!value) return; + dispatch(updateTodoContent(id, value)); + setIsEdit(false); + } + }; + + const handleClickOutsideInput = () => { + setIsEdit(false); + }; + + useOnClickOutside(inputRef, handleClickOutsideInput); + + /** Event handlers --- end */ + + return ( +
+ {isEdit ? ( + + ) : ( + <> +
+ + +
+ + + )} +
+ ); +}; + +export default memo(TodoItem); diff --git a/src/components/todoItemList/TodoItem/__test__/TodoItem.test.tsx b/src/components/todoItemList/TodoItem/__test__/TodoItem.test.tsx new file mode 100644 index 000000000..419f79ead --- /dev/null +++ b/src/components/todoItemList/TodoItem/__test__/TodoItem.test.tsx @@ -0,0 +1,81 @@ +import { fireEvent, render, RenderResult } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { TodoStatus } from "models"; +import React from "react"; +import { deleteTodo, updateTodoStatus } from "store/action-handlers"; +import { getElementByTestId } from "utils/testUtils"; +import TodoItem, { ITodoItemProps } from "../TodoItem"; + +const onDispatch = jest.fn(); + +const props = { + dispatch: onDispatch, + id: "id 1", + user_id: "user_id 1", + content: "content testing 1", + status: TodoStatus.COMPLETED, + created_date: "2022-05-08T04:44:10.054Z", +}; + +const renderComponent = ({ ...props }: ITodoItemProps): RenderResult => { + return render(); +}; + +describe(" rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent(props); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render correcly", () => { + renderComponent(props); + const element = getElementByTestId("todo-item"); + expect(element).toBeInTheDocument(); + }); + + it("should render a input with the default class", () => { + renderComponent(props); + const element = getElementByTestId("todo-item"); + expect(element).toHaveClass("todo-item"); + }); +}); + +describe(" interacting", () => { + it("should handle onClick to trigger deleteTodo", () => { + renderComponent(props); + + const element = getElementByTestId("todo-item-delete"); + userEvent.click(element); + expect(props.dispatch).toHaveBeenCalledWith(deleteTodo(props.id)); + }); + + it("should handle onClick checkbox in order to updateTodoStatus", () => { + renderComponent(props); + + const checkboxElement = getElementByTestId("checkbox") as HTMLInputElement; + fireEvent.click(checkboxElement, { target: { checked: true } }); + expect(props.dispatch).toHaveBeenCalledWith( + updateTodoStatus(props.id, TodoStatus.ACTIVE) + ); + }); + + it("should handle onDoubleClick in order to make input becomes editable", () => { + renderComponent(props); + + const element = getElementByTestId("label-content"); + const deleteElement = getElementByTestId("todo-item-delete"); + const checkboxElement = getElementByTestId("checkbox"); + const labelElement = getElementByTestId("label-content"); + + fireEvent.doubleClick(element); + + const inputElement = getElementByTestId("input") as HTMLInputElement; + + expect(inputElement).toBeInTheDocument(); + expect(deleteElement).not.toBeInTheDocument(); + expect(labelElement).not.toBeInTheDocument(); + expect(checkboxElement).not.toBeInTheDocument(); + expect(inputElement.value).toBe(props.content); + expect(inputElement).toHaveClass("todo-item__input--editing"); + }); +}); diff --git a/src/components/todoItemList/TodoItem/__test__/__snapshots__/TodoItem.test.tsx.snap b/src/components/todoItemList/TodoItem/__test__/__snapshots__/TodoItem.test.tsx.snap new file mode 100644 index 000000000..8f6ac4cd3 --- /dev/null +++ b/src/components/todoItemList/TodoItem/__test__/__snapshots__/TodoItem.test.tsx.snap @@ -0,0 +1,64 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` rendering should match snapshot 1`] = ` +
+
+
+ + +
+ +
+
+`; diff --git a/src/components/todoItemList/TodoItem/styles.scss b/src/components/todoItemList/TodoItem/styles.scss new file mode 100644 index 000000000..1cfa955d3 --- /dev/null +++ b/src/components/todoItemList/TodoItem/styles.scss @@ -0,0 +1,51 @@ +.todo-item { + display: flex; + align-items: center; + justify-content: space-between; + + & > span { + flex: 1 1; + text-align: left; + margin-left: 8px; + } + + &__input-wrapper { + display: flex; + align-items: center; + + & > label { + margin-left: 10px; + } + } + + &__delete { + outline: none; + border: none; + width: 32px; + height: 32px; + min-width: auto; + min-height: auto; + box-shadow: none; + border-radius: 50%; + display: flex; + background-color: #fff; + justify-content: center; + align-items: center; + cursor: pointer; + + &:hover { + background-color: transparent; + } + } + + &__text { + &--completed { + text-decoration: line-through; + color: #64748b; + } + } + + &__input { + width: 100%; + } +} diff --git a/src/components/todoItemList/TodoItemList.tsx b/src/components/todoItemList/TodoItemList.tsx new file mode 100644 index 000000000..fbbddd944 --- /dev/null +++ b/src/components/todoItemList/TodoItemList.tsx @@ -0,0 +1,26 @@ +import { Todo } from "models"; +import React, { Dispatch, FC, memo } from "react"; +import { AppActions } from "store/actions"; +import "./styles.scss"; +import TodoItem from "./TodoItem/TodoItem"; + +interface ITodoItemListProps { + todos: Array; + dispatch: Dispatch; +} + +const TodoItemList: FC = ({ + todos, + dispatch, + ...props +}) => { + return ( +
+ {todos.map((todo, index) => { + return ; + })} +
+ ); +}; + +export default memo(TodoItemList); diff --git a/src/components/todoItemList/index.ts b/src/components/todoItemList/index.ts new file mode 100644 index 000000000..370394949 --- /dev/null +++ b/src/components/todoItemList/index.ts @@ -0,0 +1,2 @@ +export { default as TodoItem } from "./TodoItem/TodoItem"; +export { default as TodoItemList } from './TodoItemList'; \ No newline at end of file diff --git a/src/components/todoItemList/styles.scss b/src/components/todoItemList/styles.scss new file mode 100644 index 000000000..d512ec677 --- /dev/null +++ b/src/components/todoItemList/styles.scss @@ -0,0 +1,5 @@ +.list-todo { + display: flex; + flex-direction: column; + margin-top: 1.5rem; +} diff --git a/src/components/todoTab/TodoTab.tsx b/src/components/todoTab/TodoTab.tsx new file mode 100644 index 000000000..8a425f588 --- /dev/null +++ b/src/components/todoTab/TodoTab.tsx @@ -0,0 +1,54 @@ +import { Button } from "components/button"; +import { DATA_BUTTONS } from "components/todoTab/data"; +import { EnhanceTodoStatus } from "containers/ToDoPage"; +import React, { Dispatch, FC, memo } from "react"; +import "./styles.scss"; + +export interface ITodoTabProps { + selectedStatus: EnhanceTodoStatus; + onClickSelectStatus: Dispatch>; + onDeleteAllTodo: () => void; +} + +const TodoTab: FC = ({ + selectedStatus, + onClickSelectStatus, + onDeleteAllTodo, + ...props +}) => { + const handleClickSelectStatus = (status: EnhanceTodoStatus): void => { + onClickSelectStatus(status); + }; + + const handleDeleteAlldoto = (): void => { + if (onDeleteAllTodo) onDeleteAllTodo(); + }; + + return ( + <> +
+ {DATA_BUTTONS.map(({ id, title, status }) => ( + + ))} +
+ + + ); +}; + +export default memo(TodoTab); diff --git a/src/components/todoTab/__test__/TodoTab.test.tsx b/src/components/todoTab/__test__/TodoTab.test.tsx new file mode 100644 index 000000000..c47c016ca --- /dev/null +++ b/src/components/todoTab/__test__/TodoTab.test.tsx @@ -0,0 +1,91 @@ +import { render, RenderResult, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { getElementByTestId } from "utils/testUtils"; +import TodoTab, { ITodoTabProps } from "../TodoTab"; + +const onClickSelectStatusAction = jest.fn(); +const onDeleteAllTodoAction = jest.fn(); + +const renderComponent = ({ + selectedStatus, + onClickSelectStatus, + onDeleteAllTodo, + ...props +}: ITodoTabProps): RenderResult => { + return render( + + ); +}; + +describe(" rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent({ + selectedStatus: "ALL", + onClickSelectStatus: onClickSelectStatusAction, + onDeleteAllTodo: onDeleteAllTodoAction, + }); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render correcly", () => { + renderComponent({ + selectedStatus: "ALL", + onClickSelectStatus: onClickSelectStatusAction, + onDeleteAllTodo: onDeleteAllTodoAction, + }); + const element = getElementByTestId("todo-tab"); + const buttonElements = element.querySelectorAll("button.btn-todo"); + + const clearAllButtonElement = screen.getByRole("button", { + name: /Clear all/i, + }); + + expect(clearAllButtonElement).toBeInTheDocument(); + expect(clearAllButtonElement).toHaveClass("btn-todo--danger"); + + expect(element).toBeInTheDocument(); + expect(buttonElements).toHaveLength(3); + }); + + it("should render button with selected class", () => { + renderComponent({ + selectedStatus: "ALL", + onClickSelectStatus: onClickSelectStatusAction, + onDeleteAllTodo: onDeleteAllTodoAction, + }); + const buttonElement = screen.getByRole("button", { name: "ALL" }); + expect(buttonElement).toHaveClass("btn-todo--selected"); + }); +}); + +describe(" interacting", () => { + it("should handle onClick when clicked", () => { + renderComponent({ + selectedStatus: "ALL", + onClickSelectStatus: onClickSelectStatusAction, + onDeleteAllTodo: onDeleteAllTodoAction, + }); + const buttonElement = screen.getByRole("button", { name: /ACTIVE/i }); + userEvent.click(buttonElement); + expect(onClickSelectStatusAction).toHaveBeenCalled(); + }); + + it("should handle onClick when clear all button was clicked", () => { + renderComponent({ + selectedStatus: "ALL", + onClickSelectStatus: onClickSelectStatusAction, + onDeleteAllTodo: onDeleteAllTodoAction, + }); + const clearAllButtonElement = screen.getByRole("button", { + name: /Clear all/i, + }); + userEvent.click(clearAllButtonElement); + expect(onDeleteAllTodoAction).toHaveBeenCalled(); + }); +}); diff --git a/src/components/todoTab/__test__/__snapshots__/TodoTab.test.tsx.snap b/src/components/todoTab/__test__/__snapshots__/TodoTab.test.tsx.snap new file mode 100644 index 000000000..0842a82f5 --- /dev/null +++ b/src/components/todoTab/__test__/__snapshots__/TodoTab.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` rendering should match snapshot 1`] = ` +
+
+ + + +
+ +
+`; diff --git a/src/components/todoTab/data.ts b/src/components/todoTab/data.ts new file mode 100644 index 000000000..9aab1c6a0 --- /dev/null +++ b/src/components/todoTab/data.ts @@ -0,0 +1,22 @@ +import { TodoStatus } from "models"; +import { EnhanceTodoStatus } from "containers/ToDoPage"; + +interface IDataButton { + id: string | number; + title: string; + status: EnhanceTodoStatus; +} + +export const DATA_BUTTONS: Array = [ + { id: "ALL", title: "ALL", status: "ALL" }, + { + id: "ACTIVE", + title: "ACTIVE", + status: TodoStatus.ACTIVE, + }, + { + id: "COMPLETED", + title: "COMPLETED", + status: TodoStatus.COMPLETED, + }, +]; diff --git a/src/components/todoTab/index.ts b/src/components/todoTab/index.ts new file mode 100644 index 000000000..6d5653d96 --- /dev/null +++ b/src/components/todoTab/index.ts @@ -0,0 +1 @@ +export { default as TodoTab } from "./TodoTab"; diff --git a/src/components/todoTab/styles.scss b/src/components/todoTab/styles.scss new file mode 100644 index 000000000..643a0bccc --- /dev/null +++ b/src/components/todoTab/styles.scss @@ -0,0 +1,8 @@ +.todo-tab { + display: flex; + justify-content: center; +} + +.todo-tab > *:not(:last-child) { + margin-right: 8px; +} diff --git a/src/containers/ToDoPage.tsx b/src/containers/ToDoPage.tsx new file mode 100644 index 000000000..fc33639ae --- /dev/null +++ b/src/containers/ToDoPage.tsx @@ -0,0 +1,103 @@ +import { Checkbox } from "components/checkbox"; +import { Input } from "components/input"; +import { TodoItemList } from "components/todoItemList"; +import { TodoTab } from "components/todoTab"; +import { useStateReducer } from "hooks"; +import { TodoStatus } from "models"; +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { toast, ToastContainer } from "react-toastify"; +import Service from "service"; +import { + createTodo, + deleteAllTodos, + toggleAllTodos, +} from "store/action-handlers"; +import { isTodoActive, isTodoCompleted } from "utils"; + +export type EnhanceTodoStatus = TodoStatus | "ALL"; + +const ToDoPage = () => { + const [{ todos }, dispatch] = useStateReducer(); + const [showing, setShowing] = useState("ALL"); + const inputRef = useRef(null); + + /** Event hanlders --- start */ + + const onCreateTodo = useCallback( + async (e: React.KeyboardEvent): Promise => { + if (e.key === "Enter" && inputRef.current) { + const { value } = inputRef.current; + if (!value) { + return; + } + try { + const resp = await Service.createTodo(value); + dispatch(createTodo(resp)); + inputRef.current.value = ""; + } catch (error) { + toast.error("Something went wrong!"); + } + } + }, + [dispatch] + ); + + const onToggleAllTodo = useCallback( + (e: React.ChangeEvent): void => { + dispatch(toggleAllTodos(e.target.checked)); + }, + [dispatch] + ); + + const onDeleteAllTodo = useCallback(() => { + dispatch(deleteAllTodos()); + }, [dispatch]); + + /** Event hanlders --- end */ + + const todoTasks = useMemo( + () => + todos.filter((todo) => { + switch (showing) { + case TodoStatus.ACTIVE: + return isTodoActive(todo.status); + case TodoStatus.COMPLETED: + return isTodoCompleted(todo.status); + default: + return true; + } + }), + [showing, todos] + ); + + return ( + <> +
+
+ +
+ +
+ {todoTasks.length > 0 ? ( + + ) : ( +
+ )} + +
+
+ + + ); +}; + +export default ToDoPage; diff --git a/src/containers/__test__/ToDoPage.test.tsx b/src/containers/__test__/ToDoPage.test.tsx new file mode 100644 index 000000000..3d3cbf001 --- /dev/null +++ b/src/containers/__test__/ToDoPage.test.tsx @@ -0,0 +1,38 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import ToDoPage from "containers/ToDoPage"; +import React from "react"; +import { getElementByTestId } from "utils/testUtils"; + +const renderComponent = () => { + return render(); +}; + +describe(" rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent(); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render UI correctly", () => { + renderComponent(); + + const inputElement = getElementByTestId("input") as HTMLInputElement; + + expect(inputElement).toBeInTheDocument(); + expect(inputElement).toHaveAttribute( + "placeholder", + "What need to be done?" + ); + }); +}); + +describe(" interacting", () => { + it("should be able to type input", () => { + const defaultValue = "Doing excercises"; + + renderComponent(); + const inputElement = getElementByTestId("input") as HTMLInputElement; + fireEvent.change(inputElement, { target: { value: defaultValue } }); + expect(inputElement.value).toBe(defaultValue); + }); +}); diff --git a/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap b/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap new file mode 100644 index 000000000..6f249f1db --- /dev/null +++ b/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` rendering should match snapshot 1`] = ` +
+
+
+ +
+
+
+
+
+ + + +
+ +
+
+
+
+`; diff --git a/src/containers/index.ts b/src/containers/index.ts new file mode 100644 index 000000000..576dbd5e6 --- /dev/null +++ b/src/containers/index.ts @@ -0,0 +1 @@ +export { default as ToDoPage } from "./ToDoPage"; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 000000000..d6fd2898c --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,2 @@ +export { default as useOnClickOutside } from "./useOnClickOutside"; +export { default as useStateReducer } from "./useStateReducer"; diff --git a/src/hooks/useOnClickOutside.tsx b/src/hooks/useOnClickOutside.tsx new file mode 100644 index 000000000..5da0caa5f --- /dev/null +++ b/src/hooks/useOnClickOutside.tsx @@ -0,0 +1,30 @@ +import { EventListenerType } from "models"; +import { useEffect, RefObject } from "react"; + +type Event = MouseEvent | TouchEvent; + +const useOnClickOutside = ( + ref: RefObject, + handler: (event: Event) => void +) => { + useEffect(() => { + const listener = (event: Event) => { + const el = ref?.current; + if (!el || el.contains((event?.target as Node) || null)) { + return; + } + + handler(event); + }; + + document.addEventListener(EventListenerType.MOUSEDOWN, listener); + document.addEventListener(EventListenerType.TOUCHSTART, listener); + + return () => { + document.removeEventListener(EventListenerType.MOUSEDOWN, listener); + document.removeEventListener(EventListenerType.TOUCHSTART, listener); + }; + }, [ref, handler]); +}; + +export default useOnClickOutside; diff --git a/src/hooks/useStateReducer.tsx b/src/hooks/useStateReducer.tsx new file mode 100644 index 000000000..b3538c839 --- /dev/null +++ b/src/hooks/useStateReducer.tsx @@ -0,0 +1,23 @@ +import { LOCAL_STORAGE_KEY } from "models"; +import { Dispatch, useEffect, useReducer } from "react"; +import { AppActions } from "store/actions"; +import reducer, { AppState, initialState } from "store/reducer"; +import { getLocalStorage, setLocalStorage } from "utils/storage"; + +type TUseStateReducerReturnType = [AppState, Dispatch]; + +const useStateReducer = (): TUseStateReducerReturnType => { + const [state, dispatch] = useReducer( + reducer, + initialState, + (init) => getLocalStorage(LOCAL_STORAGE_KEY) || init + ); + + useEffect(() => { + setLocalStorage(LOCAL_STORAGE_KEY, state); + }, [state]); + + return [state, dispatch]; +}; + +export default useStateReducer; diff --git a/src/index.css b/src/index.css deleted file mode 100644 index ec2585e8c..000000000 --- a/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/src/index.tsx b/src/index.tsx index f5185c1ec..388abc32c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,14 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import './index.css'; -import App from './App'; -import * as serviceWorker from './serviceWorker'; +import React from "react"; +import ReactDOM from "react-dom"; +import "styles/index.scss"; +import App from "./App"; +import * as serviceWorker from "./serviceWorker"; ReactDOM.render( , - document.getElementById('root') + document.getElementById("root") ); // If you want your app to work offline and load faster, you can change diff --git a/src/models/eventListener.ts b/src/models/eventListener.ts new file mode 100644 index 000000000..1eb0c3242 --- /dev/null +++ b/src/models/eventListener.ts @@ -0,0 +1,4 @@ +export enum EventListenerType { + MOUSEDOWN = "mousedown", + TOUCHSTART = "touchstart", +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 000000000..f884a3220 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,5 @@ +export * from "./eventListener"; +export * from "./inputType"; +export * from "./localStorage"; +export * from "./todo"; + diff --git a/src/models/inputType.ts b/src/models/inputType.ts new file mode 100644 index 000000000..82f956d4f --- /dev/null +++ b/src/models/inputType.ts @@ -0,0 +1,4 @@ +export enum InputType { + TEXT = "text", + CHECKBOX = "checkbox", +} diff --git a/src/models/localStorage.ts b/src/models/localStorage.ts new file mode 100644 index 000000000..a4816ddb2 --- /dev/null +++ b/src/models/localStorage.ts @@ -0,0 +1 @@ +export const LOCAL_STORAGE_KEY = 'mana-do-storage'; \ No newline at end of file diff --git a/src/models/todo.ts b/src/models/todo.ts index f6ab9e951..b214c6b9f 100644 --- a/src/models/todo.ts +++ b/src/models/todo.ts @@ -1,8 +1,14 @@ export enum TodoStatus { - ACTIVE = 'ACTIVE', - COMPLETED = 'COMPLETED' + ACTIVE = "ACTIVE", + COMPLETED = "COMPLETED", } +export type TTodoStatus = TodoStatus.ACTIVE | TodoStatus.COMPLETED; + export interface Todo { - [key: string]: any -} \ No newline at end of file + id: string; + user_id: string; + content: string; + status?: TodoStatus; + created_date: string; +} diff --git a/src/service/api-frontend.ts b/src/service/api-frontend.ts index 205702c5f..855a61fff 100644 --- a/src/service/api-frontend.ts +++ b/src/service/api-frontend.ts @@ -1,29 +1,29 @@ -import { IAPI } from "./types"; -import { Todo, TodoStatus } from "../models/todo"; +import { Todo, TodoStatus } from "models/todo"; import shortid from "shortid"; +import { IAPI } from "./types"; class ApiFrontend extends IAPI { - async createTodo(content: string): Promise { - return Promise.resolve({ - content: content, - created_date: new Date().toISOString(), - status: TodoStatus.ACTIVE, - id: shortid(), - user_id: "firstUser", - } as Todo); - } + async createTodo(content: string): Promise { + return Promise.resolve({ + content: content, + created_date: new Date().toISOString(), + status: TodoStatus.ACTIVE, + id: shortid(), + user_id: "firstUser", + } as Todo); + } - async getTodos(): Promise { - return [ - { - content: "Content", - created_date: new Date().toISOString(), - status: TodoStatus.ACTIVE, - id: shortid(), - user_id: "firstUser", - } as Todo, - ]; - } + async getTodos(): Promise { + return [ + { + content: "Content", + created_date: new Date().toISOString(), + status: TodoStatus.ACTIVE, + id: shortid(), + user_id: "firstUser", + } as Todo, + ]; + } } export default new ApiFrontend(); diff --git a/src/service/api-fullstack.ts b/src/service/api-fullstack.ts index afb421ee0..6f5139b74 100644 --- a/src/service/api-fullstack.ts +++ b/src/service/api-fullstack.ts @@ -1,23 +1,22 @@ -import {IAPI} from './types'; -import {Todo} from '../models/todo'; -import axios from '../utils/axios'; -import {AxiosResponse} from 'axios'; +import { IAPI } from "./types"; +import axios from "utils/axios"; +import { AxiosResponse } from "axios"; +import { Todo } from 'models'; class ApiFullstack extends IAPI { - async createTodo(content: string): Promise { - const resp = await axios.post>(`/tasks`, { - content - }); + async createTodo(content: string): Promise { + const resp = await axios.post>(`/tasks`, { + content, + }); - return resp.data.data; - } + return resp.data.data; + } - async getTodos(): Promise> { - const resp = await axios.get>>(`/tasks`); + async getTodos(): Promise> { + const resp = await axios.get>>(`/tasks`); - return resp.data.data; - } + return resp.data.data; + } } - -export default new ApiFullstack(); \ No newline at end of file +export default new ApiFullstack(); diff --git a/src/service/index.ts b/src/service/index.ts index ae5c74a00..958b6e930 100644 --- a/src/service/index.ts +++ b/src/service/index.ts @@ -1,10 +1,10 @@ -import {IAPI} from './types'; +import { IAPI } from "./types"; -let Service : IAPI; -if (process.env.REACT_APP_WHOAMI === 'frontend') { - Service = require('./api-frontend').default as IAPI +let Service: IAPI; +if (process.env.REACT_APP_WHOAMI === "frontend") { + Service = require("./api-frontend").default as IAPI; } else { - Service = require('./api-fullstack').default as IAPI + Service = require("./api-fullstack").default as IAPI; } -export default Service \ No newline at end of file +export default Service; diff --git a/src/service/types.ts b/src/service/types.ts index db7f5bf2d..0ed31d405 100644 --- a/src/service/types.ts +++ b/src/service/types.ts @@ -1,6 +1,6 @@ -import {Todo} from '../models/todo'; +import { Todo } from "models"; export abstract class IAPI { - abstract getTodos() : Promise> - abstract createTodo(content: string) : Promise -} \ No newline at end of file + abstract getTodos(): Promise>; + abstract createTodo(content: string): Promise; +} diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 000000000..48279eee4 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,2 @@ +import '@testing-library/jest-dom'; +import '@testing-library/jest-dom/extend-expect'; \ No newline at end of file diff --git a/src/store/action-handlers.ts b/src/store/action-handlers.ts new file mode 100644 index 000000000..beb7803fe --- /dev/null +++ b/src/store/action-handlers.ts @@ -0,0 +1,81 @@ +import { + SET_TODO, + CREATE_TODO, + DELETE_TODO, + DELETE_ALL_TODOS, + TOGGLE_ALL_TODOS, + UPDATE_TODO_STATUS, + UPDATE_TODO_CONTENT, +} from "./action-types"; + +import { + SetTodoAction, + CreateTodoAction, + UpdateTodoStatusAction, + DeleteTodoAction, + DeleteAllTodosAction, + ToggleAllTodosAction, + UpdateTodoContentAction, +} from "./actions"; + +import { Todo, TTodoStatus } from "models"; + +export function setTodos(todos: Array): SetTodoAction { + return { + type: SET_TODO, + payload: todos, + }; +} + +export function createTodo(newTodo: Todo): CreateTodoAction { + return { + type: CREATE_TODO, + payload: newTodo, + }; +} + +export function updateTodoStatus( + todoId: string, + status: TTodoStatus +): UpdateTodoStatusAction { + return { + type: UPDATE_TODO_STATUS, + payload: { + todoId, + status, + }, + }; +} + +export function updateTodoContent( + todoId: string, + content: string +): UpdateTodoContentAction { + return { + type: UPDATE_TODO_CONTENT, + payload: { + todoId, + content, + }, + }; +} + +export function deleteTodo(todoId: string): DeleteTodoAction { + return { + type: DELETE_TODO, + payload: todoId, + }; +} + +export function deleteAllTodos(): DeleteAllTodosAction { + return { + type: DELETE_ALL_TODOS, + }; +} + +export function toggleAllTodos(checked: boolean): ToggleAllTodosAction { + return { + type: TOGGLE_ALL_TODOS, + payload: checked, + }; +} diff --git a/src/store/action-types.ts b/src/store/action-types.ts new file mode 100644 index 000000000..6c7417016 --- /dev/null +++ b/src/store/action-types.ts @@ -0,0 +1,7 @@ +export const SET_TODO = "SET_TODO"; +export const CREATE_TODO = "CREATE_TODO"; +export const DELETE_TODO = "DELETE_TODO"; +export const DELETE_ALL_TODOS = "DELETE_ALL_TODOS"; +export const TOGGLE_ALL_TODOS = "TOGGLE_ALL_TODOS"; +export const UPDATE_TODO_STATUS = "UPDATE_TODO_STATUS"; +export const UPDATE_TODO_CONTENT = "UPDATE_TODO_CONTENT"; diff --git a/src/store/actions.ts b/src/store/actions.ts index 59e59c200..68d9f2c00 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,98 +1,59 @@ -import {Todo} from "../models/todo"; - -export const SET_TODO = 'SET_TODO'; -export const CREATE_TODO = 'CREATE_TODO'; -export const DELETE_TODO = 'DELETE_TODO'; -export const DELETE_ALL_TODOS = 'DELETE_ALL_TODOS'; -export const TOGGLE_ALL_TODOS = 'TOGGLE_ALL_TODOS'; -export const UPDATE_TODO_STATUS = 'UPDATE_TODO_STATUS'; - +import { Todo, TTodoStatus } from "models"; +import { + CREATE_TODO, + DELETE_ALL_TODOS, + DELETE_TODO, + SET_TODO, + TOGGLE_ALL_TODOS, + UPDATE_TODO_CONTENT, + UPDATE_TODO_STATUS, +} from "./action-types"; export interface SetTodoAction { - type: typeof SET_TODO, - payload: Array -} - -export function setTodos(todos: Array): SetTodoAction { - return { - type: SET_TODO, - payload: todos - } + type: typeof SET_TODO; + payload: Array; } -/////////// export interface CreateTodoAction { - type: typeof CREATE_TODO, - payload: Todo + type: typeof CREATE_TODO; + payload: Todo; } -export function createTodo(newTodo: Todo): CreateTodoAction { - return { - type: CREATE_TODO, - payload: newTodo - } -} - -////////////// export interface UpdateTodoStatusAction { - type: typeof UPDATE_TODO_STATUS, + type: typeof UPDATE_TODO_STATUS; payload: { - todoId: string, - checked: boolean - } + todoId: string; + status: TTodoStatus; + }; } -export function updateTodoStatus(todoId: string, checked: boolean): UpdateTodoStatusAction { - return { - type: UPDATE_TODO_STATUS, - payload: { - todoId, - checked - } - } +export interface UpdateTodoContentAction { + type: typeof UPDATE_TODO_CONTENT; + payload: { + todoId: string; + content: string; + }; } -////////////// export interface DeleteTodoAction { - type: typeof DELETE_TODO, - payload: string + type: typeof DELETE_TODO; + payload: string; } -export function deleteTodo(todoId: string): DeleteTodoAction { - return { - type: DELETE_TODO, - payload: todoId - } -} - -////////////// export interface DeleteAllTodosAction { - type: typeof DELETE_ALL_TODOS, -} - -export function deleteAllTodos(): DeleteAllTodosAction { - return { - type: DELETE_ALL_TODOS, - } + type: typeof DELETE_ALL_TODOS; } -/////////// export interface ToggleAllTodosAction { - type: typeof TOGGLE_ALL_TODOS, - payload: boolean -} - -export function toggleAllTodos(checked: boolean): ToggleAllTodosAction { - return { - type: TOGGLE_ALL_TODOS, - payload: checked - } + type: typeof TOGGLE_ALL_TODOS; + payload: boolean; } export type AppActions = - SetTodoAction | - CreateTodoAction | - UpdateTodoStatusAction | - DeleteTodoAction | - DeleteAllTodosAction | - ToggleAllTodosAction; \ No newline at end of file + | SetTodoAction + | CreateTodoAction + | UpdateTodoStatusAction + | DeleteTodoAction + | DeleteAllTodosAction + | ToggleAllTodosAction + | UpdateTodoContentAction; diff --git a/src/store/reducer.ts b/src/store/reducer.ts index a25f65859..c26d446c4 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,67 +1,94 @@ -import {Todo, TodoStatus} from '../models/todo'; +import { Todo } from "models"; +import { TodoStatus } from "../models"; import { - AppActions, CREATE_TODO, DELETE_ALL_TODOS, DELETE_TODO, TOGGLE_ALL_TODOS, - UPDATE_TODO_STATUS -} from './actions'; + UPDATE_TODO_CONTENT, + UPDATE_TODO_STATUS, +} from "./action-types"; +import { AppActions } from "./actions"; export interface AppState { - todos: Array + todos: Array; } export const initialState: AppState = { - todos: [] -} + todos: [], +}; function reducer(state: AppState, action: AppActions): AppState { switch (action.type) { - case CREATE_TODO: - state.todos.push(action.payload); + case CREATE_TODO: { return { - ...state + ...state, + todos: [...state.todos, action.payload], }; - - case UPDATE_TODO_STATUS: - const index2 = state.todos.findIndex((todo) => todo.id === action.payload.todoId); - state.todos[index2].status = action.payload.checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE; + } + case UPDATE_TODO_STATUS: { + const { todoId, status } = action.payload; + const tmpTodos = state.todos.map((todo) => { + if (todo.id === todoId) { + return { + ...todo, + status, + }; + } + return todo; + }); return { ...state, - todos: state.todos - } - - case TOGGLE_ALL_TODOS: - const tempTodos = state.todos.map((e)=>{ + todos: tmpTodos, + }; + } + case TOGGLE_ALL_TODOS: { + const tmpTodos = state.todos.map((e) => { return { ...e, - status: action.payload ? TodoStatus.COMPLETED : TodoStatus.ACTIVE - } - }) + status: action.payload ? TodoStatus.COMPLETED : TodoStatus.ACTIVE, + }; + }); return { ...state, - todos: tempTodos - } - - case DELETE_TODO: - const index1 = state.todos.findIndex((todo) => todo.id === action.payload); - state.todos.splice(index1, 1); - + todos: tmpTodos, + }; + } + case DELETE_TODO: { return { ...state, - todos: state.todos - } - case DELETE_ALL_TODOS: + todos: state.todos.filter((item) => item.id !== action.payload), + }; + } + case DELETE_ALL_TODOS: { return { ...state, - todos: [] - } + todos: [], + }; + } + case UPDATE_TODO_CONTENT: { + const { todoId, content } = action.payload; + + const tmpTodos = state.todos.map((item) => { + if (item.id === todoId) { + return { + ...item, + content, + }; + } + return item; + }); + + return { + ...state, + todos: tmpTodos, + }; + } default: return state; } } -export default reducer; \ No newline at end of file +export default reducer; diff --git a/src/styles/index.scss b/src/styles/index.scss new file mode 100644 index 000000000..915f7bc26 --- /dev/null +++ b/src/styles/index.scss @@ -0,0 +1,15 @@ +@import "./variables.scss"; + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", + "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss new file mode 100644 index 000000000..25da78d85 --- /dev/null +++ b/src/styles/variables.scss @@ -0,0 +1,6 @@ +:root { + --color-primary: #36af65; + --color-primary-light: #b2f3cb; + --color-primary-dark: #06842c; + --color-secondary: #cc0100; +} diff --git a/src/utils/axios.ts b/src/utils/axios.ts index 14cf39dea..e1b1852bd 100644 --- a/src/utils/axios.ts +++ b/src/utils/axios.ts @@ -1,14 +1,14 @@ -import axios from 'axios'; +import axios from "axios"; const ins = axios.create({ - baseURL: 'http://localhost:5050', - timeout: 10000 -}) + baseURL: "http://localhost:5050", + timeout: 10000, +}); -ins.interceptors.request.use((request)=>{ - request.headers.Authorization = localStorage.getItem('token') +ins.interceptors.request.use((request) => { + request.headers.Authorization = localStorage.getItem("token"); - return request -}) + return request; +}); -export default ins \ No newline at end of file +export default ins; diff --git a/src/utils/index.ts b/src/utils/index.ts index bcb4b6d32..a88429d1b 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,9 +1,17 @@ -import {Todo, TodoStatus} from '../models/todo'; +import { TodoStatus } from "models"; -export function isTodoCompleted(todo: Todo): boolean { - return todo.status === TodoStatus.COMPLETED; +export function isTodoCompleted(status: TodoStatus | undefined): boolean { + return status === TodoStatus.COMPLETED; } -export function isTodoActive(todo: Todo): boolean { - return todo.status === TodoStatus.ACTIVE; -} \ No newline at end of file +export function isTodoActive(status: TodoStatus | undefined): boolean { + return status === TodoStatus.ACTIVE; +} + +export function changeTodoStatus(checked: boolean): TodoStatus { + return checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE; +} + +export function getTodoStatus(checked: boolean): TodoStatus { + return checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE; +} diff --git a/src/utils/storage.ts b/src/utils/storage.ts new file mode 100644 index 000000000..bd1ebd262 --- /dev/null +++ b/src/utils/storage.ts @@ -0,0 +1,27 @@ +export function setLocalStorage(key: string, data?: T): void { + if (!data) { + return; + } + + try { + const serializedData = + typeof data === "string" ? data : JSON.stringify(data); + localStorage.setItem(key, serializedData); + } catch (error) { + throw new Error("Something went wrong when storing data!"); + } +} + +export function getLocalStorage(key: string): T | undefined { + try { + const serializedData = localStorage.getItem(key); + if (serializedData === null) { + return; + } + return JSON.parse(serializedData) + ? JSON.parse(serializedData) + : serializedData; + } catch (error) { + throw new Error("Something when wrong when getting data!"); + } +} diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts new file mode 100644 index 000000000..110f45eb5 --- /dev/null +++ b/src/utils/testUtils.ts @@ -0,0 +1,13 @@ +import { screen } from "@testing-library/react"; + +export function getElementByTestId(name: string): HTMLElement { + return screen.getByTestId(name); +} + +export function getAllElementByTestId(name: string): HTMLElement[] { + return screen.getAllByTestId(name); +} + +export function getElementByRole(role: string): HTMLElement { + return screen.getByRole(role); +} diff --git a/tsconfig.json b/tsconfig.json index fbce9be23..975827946 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -18,9 +14,8 @@ "isolatedModules": true, "noEmit": true, "jsx": "react", - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "baseUrl": "./src" }, - "include": [ - "src" - ] + "include": ["src/**/*"] } diff --git a/yarn.lock b/yarn.lock index 4b18892e8..535bc0b39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1407,6 +1407,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.12.5", "@babel/runtime@^7.9.2": + version "7.17.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.0.tgz#337eda67401f5b066a6f205a3113d4ac18ba495b" @@ -1984,11 +1991,60 @@ "@svgr/plugin-svgo" "^5.5.0" loader-utils "^2.0.0" +"@testing-library/dom@^8.0.0", "@testing-library/dom@^8.11.1": + version "8.13.0" + resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5" + integrity sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^5.0.0" + chalk "^4.1.0" + dom-accessibility-api "^0.5.9" + lz-string "^1.4.4" + pretty-format "^27.0.2" + +"@testing-library/jest-dom@^5.16.1": + version "5.16.4" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz#938302d7b8b483963a3ae821f1c0808f872245cd" + integrity sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@12.1.2": + version "12.1.2" + resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-12.1.2.tgz#f1bc9a45943461fa2a598bb4597df1ae044cfc76" + integrity sha512-ihQiEOklNyHIpo2Y8FREkyD1QAea054U0MVbwH1m8N9TxeFz+KoJ9LkqoKqJlzx2JDm56DVwaJ1r36JYxZM05g== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^8.0.0" + +"@testing-library/user-event@^13.5.0": + version "13.5.0" + resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" + integrity sha512-5Kwtbo3Y/NowpkbRuSepbyMFkZmHgD+vPzYB/RJ4oxt5Gj/avFFBYjhw27cqSVPVw/3a67NK1PbiIr9k4Gwmdg== + dependencies: + "@babel/runtime" "^7.12.5" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.7": version "7.1.17" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.17.tgz#f50ac9d20d64153b510578d84f9643f9a3afbe64" @@ -2110,6 +2166,14 @@ dependencies: "@types/istanbul-lib-report" "*" +"@types/jest@*": + version "27.5.0" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.5.0.tgz#e04ed1824ca6b1dd0438997ba60f99a7405d4c7b" + integrity sha512-9RBFx7r4k+msyj/arpfaa0WOOEcaAZNmN+j80KFbFCoSqCJGHTz7YMAMGQW9Xmqm5w6l5c25vbSjMwlikJi5+g== + dependencies: + jest-matcher-utils "^27.0.0" + pretty-format "^27.0.0" + "@types/jest@^24.0.0": version "24.9.1" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" @@ -2214,6 +2278,13 @@ resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" integrity sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ== +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.3" + resolved "https://registry.yarnpkg.com/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz#ee6c7ffe9f8595882ee7bda8af33ae7b8789ef17" + integrity sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw== + dependencies: + "@types/jest" "*" + "@types/uglify-js@*": version "3.13.1" resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.13.1.tgz#5e889e9e81e94245c75b6450600e1c5ea2878aea" @@ -2723,6 +2794,11 @@ ansi-styles@^4.1.0: "@types/color-name" "^1.1.1" color-convert "^2.0.1" +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" @@ -2759,6 +2835,11 @@ aria-query@^4.2.2: "@babel/runtime" "^7.10.2" "@babel/runtime-corejs3" "^7.10.2" +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + arity-n@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745" @@ -3568,7 +3649,15 @@ chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.0.0: +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -3586,6 +3675,21 @@ check-types@^11.1.1: resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== +"chokidar@>=3.0.0 <4.0.0": + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3695,6 +3799,11 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +clsx@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -4158,6 +4267,11 @@ css-what@^3.2.1: resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + css@^2.0.0: version "2.2.4" resolved "https://registry.yarnpkg.com/css/-/css-2.2.4.tgz#c646755c73971f2bba6a601e2cf2fd71b1298929" @@ -4168,6 +4282,15 @@ css@^2.0.0: source-map-resolve "^0.5.2" urix "^0.1.0" +css@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/css/-/css-3.0.0.tgz#4447a4d58fdd03367c516ca9f64ae365cee4aa5d" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + cssdb@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0" @@ -4497,6 +4620,11 @@ diff-sequences@^26.6.2: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== +diff-sequences@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" + integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -4547,6 +4675,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: + version "0.5.14" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56" + integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg== + dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" @@ -6170,6 +6303,11 @@ immer@8.0.1: resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656" integrity sha512-aqXhGP7//Gui2+UrEtvxZxSquQVXTpZ7KDxfCcKAF3Vysvw0CViVaW9RZ1j1xlIYqaaaipBoqdqeibkc18PNvA== +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + import-cwd@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" @@ -6873,6 +7011,16 @@ jest-diff@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-diff@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" + integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-docblock@^26.0.0: version "26.0.0" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-26.0.0.tgz#3e2fa20899fc928cb13bd0ff68bd3711a36889b5" @@ -6926,6 +7074,11 @@ jest-get-type@^26.3.0: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== +jest-get-type@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" + integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw== + jest-haste-map@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" @@ -6989,6 +7142,16 @@ jest-matcher-utils@^26.6.0, jest-matcher-utils@^26.6.2: jest-get-type "^26.3.0" pretty-format "^26.6.2" +jest-matcher-utils@^27.0.0: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" + integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== + dependencies: + chalk "^4.0.0" + jest-diff "^27.5.1" + jest-get-type "^27.5.1" + pretty-format "^27.5.1" + jest-message-util@^26.6.0, jest-message-util@^26.6.2: version "26.6.2" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" @@ -7594,6 +7757,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + magic-string@^0.25.0, magic-string@^0.25.7: version "0.25.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" @@ -7776,6 +7944,11 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + mini-css-extract-plugin@0.11.3: version "0.11.3" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.11.3.tgz#15b0910a7f32e62ffde4a7430cfefbd700724ea6" @@ -9434,6 +9607,15 @@ pretty-format@^26.6.0, pretty-format@^26.6.2: ansi-styles "^4.0.0" react-is "^17.0.1" +pretty-format@^27.0.0, pretty-format@^27.0.2, pretty-format@^27.5.1: + version "27.5.1" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e" + integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" @@ -9698,6 +9880,11 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== +react-icons@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.3.1.tgz#2fa92aebbbc71f43d2db2ed1aed07361124e91ca" + integrity sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ== + react-is@^16.8.1, react-is@^16.8.4: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -9779,6 +9966,13 @@ react-scripts@4.0.3: optionalDependencies: fsevents "^2.1.3" +react-toastify@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.0.1.tgz#2f3abd26a75efd55a82cb9c0a897c865218ea420" + integrity sha512-c2zeZHkCX+WXuItS/JRqQ/8CH8Qm/je+M0rt09xe9fnu5YPJigtNOdD8zX4fwLA093V2am3abkGfOowwpkrwOQ== + dependencies: + clsx "^1.1.1" + react@^16.13.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" @@ -9852,6 +10046,14 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -10280,6 +10482,15 @@ sass-loader@^10.0.5: schema-utils "^3.0.0" semver "^7.3.2" +sass@^1.51.0: + version "1.51.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.51.0.tgz#25ea36cf819581fe1fe8329e8c3a4eaaf70d2845" + integrity sha512-haGdpTgywJTvHC2b91GSq+clTKGbtkkZmVAb82jZQN/wTy6qs8DdFm2lhEQbEwrY0QDRgSQ3xDurqM977C3noA== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -10617,6 +10828,11 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + source-map-js@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.1.tgz#a1741c131e3c77d048252adfa24e23b908670caf" @@ -10633,6 +10849,14 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.6.0.tgz#3d9df87e236b53f16d01e58150fc7711138e5ed2" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + source-map-support@^0.5.6, source-map-support@~0.5.12: version "0.5.16" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" @@ -10981,6 +11205,13 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" From 52b929c2d392409e371ccae685f432deac213e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=AA=20Huy=20V=C5=A9?= Date: Mon, 9 May 2022 00:25:58 +0700 Subject: [PATCH 2/2] Final commit --- src/App.scss | 29 ---- src/components/checkbox/styles.scss | 2 +- src/components/guide/Guide.tsx | 14 ++ src/components/guide/__test__/Guide.test.tsx | 25 ++++ .../__snapshots__/Guide.test.tsx.snap | 15 ++ src/components/guide/index.ts | 1 + src/components/guide/styles.scss | 6 + src/components/header/Header.tsx | 14 ++ .../header/__test__/Header.test.tsx | 23 +++ .../__snapshots__/Header.test.tsx.snap | 36 +++++ src/components/header/index.ts | 1 + src/components/header/styles.scss | 10 ++ src/containers/ToDoPage.tsx | 17 ++- src/containers/__test__/ToDoPage.test.tsx | 2 +- .../__snapshots__/ToDoPage.test.tsx.snap | 131 ++++++++++++------ src/containers/styles.scss | 30 ++++ src/styles/variables.scss | 3 +- 17 files changed, 277 insertions(+), 82 deletions(-) create mode 100644 src/components/guide/Guide.tsx create mode 100644 src/components/guide/__test__/Guide.test.tsx create mode 100644 src/components/guide/__test__/__snapshots__/Guide.test.tsx.snap create mode 100644 src/components/guide/index.ts create mode 100644 src/components/guide/styles.scss create mode 100644 src/components/header/Header.tsx create mode 100644 src/components/header/__test__/Header.test.tsx create mode 100644 src/components/header/__test__/__snapshots__/Header.test.tsx.snap create mode 100644 src/components/header/index.ts create mode 100644 src/components/header/styles.scss create mode 100644 src/containers/styles.scss diff --git a/src/App.scss b/src/App.scss index 6a4cf769b..377086005 100644 --- a/src/App.scss +++ b/src/App.scss @@ -15,32 +15,3 @@ input { input:focus { box-shadow: 1px 0 9px rgba(0, 0, 0, 0.25); } - -.ToDo__container { - border: 1px solid rgba(0, 0, 0, 0.13); - border-radius: 8px; - width: 500px; - margin-top: 5rem; - padding: 24px; - box-shadow: 2px 2px 1px rgba(0, 0, 0, 0.09), 3px 2px 3px rgba(0, 0, 0, 0.05); -} - -.Todo__creation { - display: flex; -} - -.Todo__content { - flex: 1 1; -} - -.Todo__action { - width: 24px; - height: 24px; - flex-shrink: 0; -} - -.Todo__toolbar { - display: flex; - justify-content: space-between; - margin-top: 24px; -} diff --git a/src/components/checkbox/styles.scss b/src/components/checkbox/styles.scss index db4775eb9..dd70e8986 100644 --- a/src/components/checkbox/styles.scss +++ b/src/components/checkbox/styles.scss @@ -4,7 +4,7 @@ height: 30px; color: #363839; border: 1px solid #bdc1c6; - border-radius: 4px; + border-radius: 50%; appearance: none; outline: 0; cursor: pointer; diff --git a/src/components/guide/Guide.tsx b/src/components/guide/Guide.tsx new file mode 100644 index 000000000..9f9c1432f --- /dev/null +++ b/src/components/guide/Guide.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import "./styles.scss"; + +const Guid = () => { + return ( +
+ + * Double-click to the label to edit the corresponding task! + +
+ ); +}; + +export default Guid; diff --git a/src/components/guide/__test__/Guide.test.tsx b/src/components/guide/__test__/Guide.test.tsx new file mode 100644 index 000000000..17cee346d --- /dev/null +++ b/src/components/guide/__test__/Guide.test.tsx @@ -0,0 +1,25 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import Guide from "../Guide"; + +const renderComponent = () => { + return render(); +}; + +describe(" rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent(); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render UI correctly", () => { + renderComponent(); + + const titleElement = screen.getByText( + "* Double-click to the label to edit the corresponding task!" + ); + + expect(titleElement).toBeInTheDocument(); + expect(titleElement).toHaveClass("guide__text"); + }); +}); diff --git a/src/components/guide/__test__/__snapshots__/Guide.test.tsx.snap b/src/components/guide/__test__/__snapshots__/Guide.test.tsx.snap new file mode 100644 index 000000000..5e2c2ab05 --- /dev/null +++ b/src/components/guide/__test__/__snapshots__/Guide.test.tsx.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` rendering should match snapshot 1`] = ` +
+
+ + * Double-click to the label to edit the corresponding task! + +
+
+`; diff --git a/src/components/guide/index.ts b/src/components/guide/index.ts new file mode 100644 index 000000000..318da21fe --- /dev/null +++ b/src/components/guide/index.ts @@ -0,0 +1 @@ +export { default as Guide } from "./Guide"; diff --git a/src/components/guide/styles.scss b/src/components/guide/styles.scss new file mode 100644 index 000000000..98709a285 --- /dev/null +++ b/src/components/guide/styles.scss @@ -0,0 +1,6 @@ +.guide { + margin-top: 15px; + &__text { + color: #888888; + } +} diff --git a/src/components/header/Header.tsx b/src/components/header/Header.tsx new file mode 100644 index 000000000..b0ac2a41a --- /dev/null +++ b/src/components/header/Header.tsx @@ -0,0 +1,14 @@ +import React from "react"; +import { MdTaskAlt } from "react-icons/md"; +import "./styles.scss"; + +const Header = () => { + return ( +
+ +

MANA DO

+
+ ); +}; + +export default Header; diff --git a/src/components/header/__test__/Header.test.tsx b/src/components/header/__test__/Header.test.tsx new file mode 100644 index 000000000..d764ad061 --- /dev/null +++ b/src/components/header/__test__/Header.test.tsx @@ -0,0 +1,23 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import Header from "../Header"; + +const renderComponent = () => { + return render(
); +}; + +describe("
rendering", () => { + it("should match snapshot", () => { + const wrapper = renderComponent(); + expect(wrapper.container).toMatchSnapshot(); + }); + + it("should render UI correctly", () => { + renderComponent(); + + const titleElement = screen.getByText("MANA DO"); + + expect(titleElement).toBeInTheDocument(); + expect(titleElement).toHaveClass("header__title"); + }); +}); diff --git a/src/components/header/__test__/__snapshots__/Header.test.tsx.snap b/src/components/header/__test__/__snapshots__/Header.test.tsx.snap new file mode 100644 index 000000000..0f64bbe0c --- /dev/null +++ b/src/components/header/__test__/__snapshots__/Header.test.tsx.snap @@ -0,0 +1,36 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`
rendering should match snapshot 1`] = ` +
+
+ + + + +

+ MANA DO +

+
+
+`; diff --git a/src/components/header/index.ts b/src/components/header/index.ts new file mode 100644 index 000000000..d88c989bb --- /dev/null +++ b/src/components/header/index.ts @@ -0,0 +1 @@ +export { default as Header } from "./Header"; diff --git a/src/components/header/styles.scss b/src/components/header/styles.scss new file mode 100644 index 000000000..81d41aeb9 --- /dev/null +++ b/src/components/header/styles.scss @@ -0,0 +1,10 @@ +.header { + display: flex; + align-items: center; + justify-content: center; + + &__title { + color: var(--color-mana); + margin-left: 10px; + } +} diff --git a/src/containers/ToDoPage.tsx b/src/containers/ToDoPage.tsx index fc33639ae..6e85a75a4 100644 --- a/src/containers/ToDoPage.tsx +++ b/src/containers/ToDoPage.tsx @@ -1,4 +1,6 @@ import { Checkbox } from "components/checkbox"; +import { Guide } from "components/guide"; +import { Header } from "components/header"; import { Input } from "components/input"; import { TodoItemList } from "components/todoItemList"; import { TodoTab } from "components/todoTab"; @@ -13,6 +15,7 @@ import { toggleAllTodos, } from "store/action-handlers"; import { isTodoActive, isTodoCompleted } from "utils"; +import "./styles.scss"; export type EnhanceTodoStatus = TodoStatus | "ALL"; @@ -71,9 +74,10 @@ const ToDoPage = () => { ); return ( - <> -
-
+
+
+
+
{ />
-
+
{todoTasks.length > 0 ? ( - + ) : (
)} @@ -96,7 +100,8 @@ const ToDoPage = () => {
- + +
); }; diff --git a/src/containers/__test__/ToDoPage.test.tsx b/src/containers/__test__/ToDoPage.test.tsx index 3d3cbf001..6548df5e2 100644 --- a/src/containers/__test__/ToDoPage.test.tsx +++ b/src/containers/__test__/ToDoPage.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react"; +import { fireEvent, render } from "@testing-library/react"; import ToDoPage from "containers/ToDoPage"; import React from "react"; import { getElementByTestId } from "utils/testUtils"; diff --git a/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap b/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap index 6f249f1db..e7c3c8be2 100644 --- a/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap +++ b/src/containers/__test__/__snapshots__/ToDoPage.test.tsx.snap @@ -3,64 +3,107 @@ exports[` rendering should match snapshot 1`] = `
- -
-
-
-
- - + MANA DO + +
+
+ +
+
+
+
+
+ + + +
- + * Double-click to the label to edit the corresponding task! +
-
`; diff --git a/src/containers/styles.scss b/src/containers/styles.scss new file mode 100644 index 000000000..3dc9880f5 --- /dev/null +++ b/src/containers/styles.scss @@ -0,0 +1,30 @@ +.todo { + &__container { + border: 1px solid rgba(0, 0, 0, 0.13); + border-radius: 8px; + width: 500px; + margin-top: 5rem; + padding: 24px; + box-shadow: 2px 2px 1px rgba(0, 0, 0, 0.09), 3px 2px 3px rgba(0, 0, 0, 0.05); + } + + &__creation { + display: flex; + } + + &__toolbar { + display: flex; + justify-content: space-between; + margin-top: 24px; + } + + &__action { + width: 24px; + height: 24px; + flex-shrink: 0; + } + + &__content { + flex: 1 1; + } +} diff --git a/src/styles/variables.scss b/src/styles/variables.scss index 25da78d85..86ff18ebe 100644 --- a/src/styles/variables.scss +++ b/src/styles/variables.scss @@ -3,4 +3,5 @@ --color-primary-light: #b2f3cb; --color-primary-dark: #06842c; --color-secondary: #cc0100; -} + --color-mana: #3A52D1; +} \ No newline at end of file