diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 000000000..a50f0807c --- /dev/null +++ b/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', {targets: {node: 'current'}}], + '@babel/preset-typescript', + ], + }; \ No newline at end of file diff --git a/package.json b/package.json index 699d92678..4907c8052 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ ] }, "devDependencies": { + "@babel/preset-typescript": "^7.16.7", "@types/shortid": "^0.0.29" } } diff --git a/src/App.css b/src/App.css index cc74a3fea..180808455 100644 --- a/src/App.css +++ b/src/App.css @@ -122,4 +122,8 @@ input:focus { } .Action__btn { +} +.Todo__done { + text-decoration: line-through; + pointer-events: none; } \ No newline at end of file diff --git a/src/ToDoPage.tsx b/src/ToDoPage.tsx index 1909718d0..3af3bb79d 100644 --- a/src/ToDoPage.tsx +++ b/src/ToDoPage.tsx @@ -1,107 +1,134 @@ -import React, {useEffect, useReducer, useRef, useState} from 'react'; +import React, { useEffect, useReducer, useRef, useState } from "react"; -import reducer, {initialState} from './store/reducer'; +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'; + setTodos, + createTodo, + toggleAllTodos, + deleteAllTodos, + updateTodoStatus, + deleteTodo, + updateTodo, + todoActive, + todoComplete, +} from "./store/actions"; +import Service from "./service"; +import { TodoStatus } from "./models/todo"; const ToDoPage = () => { - const [{todos}, dispatch] = useReducer(reducer, initialState); - const [showing, setShowing] = useState('ALL'); - const inputRef = useRef(null); + const [{ todos }, dispatch] = useReducer(reducer, initialState); + const inputRef = useRef(null); - useEffect(()=>{ - (async ()=>{ - const resp = await Service.getTodos(); + useEffect(() => { + (async () => { + const resp = await Service.getTodos(); - dispatch(setTodos(resp || [])); - })() - }, []) + 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 onCreateTodo = async (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + if (inputRef.current.value.length > 0) { + const resp = await Service.createTodo(inputRef.current.value.trim()); + dispatch(createTodo(resp)); + localStorage.setItem("todo-app", JSON.stringify([...todos, resp])); + onResetInput(); + } else alert("Please input your task"); } + }; - const onUpdateTodoStatus = (e: React.ChangeEvent, todoId: any) => { - dispatch(updateTodoStatus(todoId, e.target.checked)) - } + const onResetInput = () => { + inputRef.current.value = ""; + }; - const onToggleAllTodo = (e: React.ChangeEvent) => { - dispatch(toggleAllTodos(e.target.checked)) - } + const onUpdateTodoStatus = ( e: React.ChangeEvent, todoId: any ) => { + dispatch(updateTodoStatus(todoId, e.target.checked)); + }; - const onDeleteAllTodo = () => { - dispatch(deleteAllTodos()); - } + 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 ? - :
- } -
- - - -
- + const onDeleteTodo = (todoId: string) => { + dispatch(deleteTodo(todoId)); + }; + + const onUpdateTodo = ( e: React.ChangeEvent, todoId: string ) => { + dispatch(updateTodo(e.target.value, todoId)); + }; + + + return ( +
+
+ +
+
+ {todos.map((todo, index) => { + return ( +
+ onUpdateTodoStatus(e, todo.id)} + /> + onUpdateTodo(e, todo.id)} + className={`${todo.status === TodoStatus.COMPLETED && 'Todo__done'}`} + /> + +
+ ); + })} +
+
+ {todos.length > 0 ? ( + + ) : ( +
+ )} +
+ + +
- ); + +
+
+ ); }; -export default ToDoPage; \ No newline at end of file +export default ToDoPage; diff --git a/src/store/actions.ts b/src/store/actions.ts index 59e59c200..b8ba3e150 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,98 +1,145 @@ -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 } 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"; +export const UPDATE_TODO = "UPDATE_TODO"; +export const TODO_ACTIVE = "TODO_ACTIVE"; +export const TODO_COMPLETE = "TODO_COMPLETE"; export interface SetTodoAction { - type: typeof SET_TODO, - payload: Array + type: typeof SET_TODO; + payload: Array; } export function setTodos(todos: Array): SetTodoAction { return { type: SET_TODO, - payload: todos - } + payload: todos, + }; } /////////// 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 - } + payload: newTodo, + }; } ////////////// export interface UpdateTodoStatusAction { - type: typeof UPDATE_TODO_STATUS, + type: typeof UPDATE_TODO_STATUS; payload: { - todoId: string, - checked: boolean - } + todoId: string; + checked: boolean; + }; } -export function updateTodoStatus(todoId: string, checked: boolean): UpdateTodoStatusAction { +export function updateTodoStatus( + todoId: string, + checked: boolean +): UpdateTodoStatusAction { return { type: UPDATE_TODO_STATUS, payload: { todoId, - checked - } - } + checked, + }, + }; +} + +export interface UpdateTodoAction { + type: typeof UPDATE_TODO; + payload: { + todoId: string; + value: string; + }; +} + +export function updateTodo(value: string, todoId: string): UpdateTodoAction { + return { + type: UPDATE_TODO, + payload: { + todoId, + value, + }, + }; } ////////////// 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 - } + payload: todoId, + }; } ////////////// export interface DeleteAllTodosAction { - type: typeof DELETE_ALL_TODOS, + type: typeof DELETE_ALL_TODOS; } export function deleteAllTodos(): DeleteAllTodosAction { return { type: DELETE_ALL_TODOS, - } + }; } /////////// export interface ToggleAllTodosAction { - type: typeof TOGGLE_ALL_TODOS, - payload: boolean + type: typeof TOGGLE_ALL_TODOS; + payload: boolean; } export function toggleAllTodos(checked: boolean): ToggleAllTodosAction { return { type: TOGGLE_ALL_TODOS, - payload: checked - } + payload: checked, + }; +} + +export interface TodoActive { + type: typeof TODO_ACTIVE; + payload: string; +} +export function todoActive(status: string): TodoActive { + return { + type: TODO_ACTIVE, + payload: status, + }; +} +export interface TodoCompleted { + type: typeof TODO_COMPLETE; + payload: string; +} +export function todoComplete(status: string): TodoCompleted { + return { + type: TODO_COMPLETE, + payload: status, + }; } export type AppActions = - SetTodoAction | - CreateTodoAction | - UpdateTodoStatusAction | - DeleteTodoAction | - DeleteAllTodosAction | - ToggleAllTodosAction; \ No newline at end of file + | SetTodoAction + | CreateTodoAction + | UpdateTodoStatusAction + | UpdateTodoAction + | DeleteTodoAction + | DeleteAllTodosAction + | ToggleAllTodosAction + | TodoActive + | TodoCompleted; diff --git a/src/store/reducer.ts b/src/store/reducer.ts index a25f65859..8a6788380 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,67 +1,121 @@ -import {Todo, TodoStatus} from '../models/todo'; +import { Todo, TodoStatus } from "../models/todo"; import { AppActions, CREATE_TODO, DELETE_ALL_TODOS, DELETE_TODO, TOGGLE_ALL_TODOS, - UPDATE_TODO_STATUS -} from './actions'; + UPDATE_TODO_STATUS, + UPDATE_TODO, + SET_TODO, + TODO_ACTIVE, + TODO_COMPLETE, +} from "./actions"; export interface AppState { - todos: Array + todos: Array; } +const storeData = (todos: any) => window.localStorage.setItem("mana-todo", JSON.stringify(todos)); export const initialState: AppState = { - todos: [] -} + todos: [], +}; +const todo = JSON.parse(window.localStorage.getItem("mana-todo") || ""); function reducer(state: AppState, action: AppActions): AppState { switch (action.type) { + + case SET_TODO: + return { ...state, todos: todo }; + case CREATE_TODO: - state.todos.push(action.payload); + const newTodo = [...state.todos, action.payload]; + window.localStorage.setItem("mana-todo", JSON.stringify(newTodo)); return { - ...state + ...state, + todos: newTodo, }; 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; - + let checkedTodo = state.todos.map((td) => { + if (td.id === action.payload.todoId) { + return { + ...td, + status: action.payload.checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE, + }; + } else { + return td; + } + }); + storeData(checkedTodo); return { ...state, - todos: state.todos - } + todos: checkedTodo, + }; + + case UPDATE_TODO: + const contentTodo = state.todos.map((todo) => { + if (todo.id === action.payload.todoId) { + return { ...todo, content: action.payload.value }; + } else { + return todo; + } + }); + storeData(contentTodo); + return { + ...state, + todos: contentTodo, + }; case TOGGLE_ALL_TODOS: - const tempTodos = state.todos.map((e)=>{ + const tempTodos = state.todos.map((e) => { return { ...e, - status: action.payload ? TodoStatus.COMPLETED : TodoStatus.ACTIVE - } - }) - + status: action.payload ? TodoStatus.COMPLETED : TodoStatus.ACTIVE, + }; + }); + storeData(tempTodos); + return { ...state, - todos: tempTodos - } + todos: tempTodos, + }; case DELETE_TODO: - const index1 = state.todos.findIndex((todo) => todo.id === action.payload); - state.todos.splice(index1, 1); + let deleteTodo = state.todos.filter((td) => td.id !== action.payload); + storeData(deleteTodo); return { ...state, - todos: state.todos - } + todos: deleteTodo, + }; + case DELETE_ALL_TODOS: + storeData([]); + return { + ...state, + todos: [], + }; + + case TODO_ACTIVE: + let activeTodo = todo.filter((todoAct: any) => todoAct.status === action.payload); + return { ...state, - todos: [] - } + todos: activeTodo, + }; + + case TODO_COMPLETE: + let completeTodo = todo.filter((td: any) => td.status === action.payload); + + return { + ...state, + todos: completeTodo, + }; + default: return state; } } -export default reducer; \ No newline at end of file +export default reducer; diff --git a/src/todo.test.js b/src/todo.test.js new file mode 100644 index 000000000..37840ed28 --- /dev/null +++ b/src/todo.test.js @@ -0,0 +1,133 @@ +import { + createTodo, + deleteAllTodos, + DELETE_ALL_TODOS, + setTodos, + SET_TODO, + updateTodo, + updateTodoStatus, + UPDATE_TODO, + UPDATE_TODO_STATUS +} from './store/actions'; +import { + CREATE_TODO +} from '../src/store/actions' +import { + TodoStatus +} from "./models/todo"; +import shortid from 'shortid'; + +let mockData = { + content: 'xxxx', + created_date: new Date().toISOString(), + status: TodoStatus.ACTIVE, + id: shortid(), + user_id: "firstUser", +} +let listTodo = [{ + content: 'xxxx', + created_date: new Date().toISOString(), + status: TodoStatus.ACTIVE, + id: shortid(), + user_id: "firstUser", +}, { + content: 'todo-1', + created_date: new Date().toISOString(), + status: TodoStatus.ACTIVE, + id: shortid(), + user_id: "firstUser", +}, { + content: 'todo-2', + created_date: new Date().toISOString(), + status: TodoStatus.ACTIVE, + id: shortid(), + user_id: "firstUser", +}]; + + +describe('get list todo ', () => { + it('should get list todo ', () => { + const result = setTodos(mockData) + expect(result).toMatchObject({ + payload: mockData, + type: SET_TODO + }) + }) +}) + +describe('addTodo', () => { + it('should add todo to the list', () => { + const result = createTodo(mockData) + expect(result).toMatchObject({ + payload: mockData, + type: CREATE_TODO + }) + }) +}) + +describe('update todo status', () => { + it('should update todo to status', () => { + const expectResult = { + ...mockData, + status: TodoStatus.COMPLETED + } + const result = updateTodoStatus(expectResult.id, true) + const finalResult = { + ...result, + payload: { + ...mockData, + status: result.payload.checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE, + id: result.payload.todoId + } + } + expect(finalResult).toMatchObject({ + payload: expectResult, + type: UPDATE_TODO_STATUS + }) + }) +}) + +describe('update todo content', () => { + it('should update todo content', () => { + const expectResult = { + ...mockData, + content: 'text' + } + const result = updateTodo('text', mockData.id) + const finalResult = { + ...result, + payload: { + ...mockData, + content: result.payload.value, + id: result.payload.todoId + } + } + expect(finalResult).toMatchObject({ + payload: expectResult, + type: UPDATE_TODO + }) + }) +}) + +// describe('DELETE TODO', () => { +// it('should delete todo to the list', () => { +// const result = deleteTodo(mockData) +// const index = listTodo.findIndex(todo => todo.id === result.payload.id) +// const expectResult = listTodo.splice(index,1) +// console.log("🚀 ~ file: todo.test.js ~ line 119 ~ it ~ expectResult", expectResult) +// expect(result).toMatchObject({ +// payload: expectResult, +// type: DELETE_TODO, +// }) +// }) +// }) + + +describe('delete all todo ', () => { + it('should delete all todo to the list', () => { + const result = deleteAllTodos() + expect(result).toMatchObject({ + type: DELETE_ALL_TODOS + }) + }) +}) \ No newline at end of file