From 060c900f69b1600b9a9a4a777d4c5e9435891f5c Mon Sep 17 00:00:00 2001 From: Linh Nguyen Date: Sun, 15 May 2022 17:37:32 +0700 Subject: [PATCH] fix some bugs, enhance UX, UI, add features --- src/App.css | 90 +++++++++-- src/App.tsx | 1 - src/ToDoPage.tsx | 330 ++++++++++++++++++++++++++++----------- src/components/Themes.js | 11 ++ src/store/actions.ts | 38 ++++- src/store/reducer.ts | 64 ++++++-- 6 files changed, 420 insertions(+), 114 deletions(-) create mode 100644 src/components/Themes.js diff --git a/src/App.css b/src/App.css index cc74a3fea..4db7c41a0 100644 --- a/src/App.css +++ b/src/App.css @@ -8,17 +8,21 @@ button { outline: none; border: none; box-shadow: 2px 0 2px currentColor; - border-radius: 4px; + border-radius: 7px; min-height: 32px; min-width: 80px; padding: 4px 8px; + font-weight: bold; + color: #fff; } button:hover { - opacity: 0.85; + opacity: 0.7; + transition: 0.3s; + cursor: pointer; } -input[type="checkbox"] { +input[type='checkbox'] { width: 24px; height: 24px; box-shadow: none; @@ -26,7 +30,7 @@ input[type="checkbox"] { outline: none; } -input[type="checkbox"]:focus { +input[type='checkbox']:focus { box-shadow: none; border: none; outline: none; @@ -37,22 +41,21 @@ input { border: none; outline: none; padding: 0 12px; - box-shadow: 2px 0 4px rgba(0,0,0, 0.2); + 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); + box-shadow: 1px 0 9px rgba(0, 0, 0, 0.25); } .ToDo__container { - border: 1px solid rgba(0,0,0, 0.13); + 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); + box-shadow: 2px 2px 1px rgba(0, 0, 0, 0.09), 3px 2px 3px rgba(0, 0, 0, 0.05); } .Todo__creation { @@ -81,8 +84,16 @@ input:focus { margin-left: 8px; } +/* adding ellipsis when text is too long */ .Todo__content { + overflow: hidden; + text-overflow: ellipsis; + width: 100%; flex: 1 1; + cursor: pointer; +} +.Todo__content:hover { + overflow: visible; } .Todo__delete { @@ -98,12 +109,11 @@ input:focus { justify-content: center; align-items: center; cursor: pointer; -} - -.Todo__action, .Todo__delete { width: 24px; height: 24px; flex-shrink: 0; + background-color: #e15353; + box-shadow: 2px 2px #e1535350; } .Todo__toolbar { @@ -121,5 +131,57 @@ input:focus { margin-right: 8px; } -.Action__btn { -} \ No newline at end of file +.Action__btn__completed { + background-color: #01c9f8; + box-shadow: 3px 3px #01c9f850; +} + +.Action__btn__active { + background-color: #51d686; + box-shadow: 3px 3px #51d68650; +} + +.Action__btn__clear { + background-color: #e15353; + box-shadow: 3px 3px #e1535350; +} + +.Error__text { + color: red; + text-align: left; + padding-top: 0.5rem; + padding-left: 0.5rem; +} + +input[type='checkbox'] { + position: relative; + cursor: pointer; + -webkit-appearance: none; +} +input[type='checkbox']:before { + content: ''; + display: block; + position: absolute; + top: 9px; + left: 0; + width: 16px; + height: 16px; + border: 1px solid rgba(204, 204, 204, 0.76); + border-radius: 3px; + background-color: #fff; +} + +input[type='checkbox']:checked:after { + content: ''; + display: block; + width: 7px; + height: 20px; + border: 2px solid #01c9f8; + border-width: 0 2px 2px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + position: absolute; + top: 0px; + left: 10px; +} diff --git a/src/App.tsx b/src/App.tsx index 2395f3d30..9545114b1 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,4 @@ import React from 'react'; - import ToDoPage from './ToDoPage'; import './App.css'; diff --git a/src/ToDoPage.tsx b/src/ToDoPage.tsx index 1909718d0..6a8570717 100644 --- a/src/ToDoPage.tsx +++ b/src/ToDoPage.tsx @@ -1,107 +1,261 @@ -import React, {useEffect, useReducer, useRef, useState} from 'react'; +import React, { + ChangeEvent, + useEffect, + useReducer, + useRef, + useState, +} from 'react'; -import reducer, {initialState} from './store/reducer'; +import reducer, { initialState } from './store/reducer'; import { - setTodos, - createTodo, - toggleAllTodos, - deleteAllTodos, - updateTodoStatus + setTodos, + createTodo, + deleteAllTodos, + updateTodoStatus, + deleteTodo, + updateTodoContent, + updateAllStatus, } from './store/actions'; import Service from './service'; -import {TodoStatus} from './models/todo'; +import { TodoStatus } from './models/todo'; +import { DarkMode, LightMode } from './components/Themes'; -type EnhanceTodoStatus = TodoStatus | 'ALL'; +const ToDoPage = () => { + const [{ todos }, dispatch] = useReducer(reducer, initialState); + const [task, setTask] = useState(''); + const [error, setError] = useState(''); + const [editMode, setEditMode] = useState(true); + const [newTask, setNewTask] = useState(''); + const [editTodoIdx, setEditTodoIdx] = useState(''); + const [editError, setEditError] = useState(''); + const [darkMode, setDarkMode] = useState(false); + const inputRef = useRef(null); + const handleChange = (event: ChangeEvent): void => { + setTask(event.target.value); + }; + const handleEdit = (event: ChangeEvent): void => { + setNewTask(event.target.value); + }; -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)); - } - } + // useEffect(()=>{ + // (async ()=>{ + // const resp = await Service.getTodos(); + // dispatch(setTodos(resp || [])); + // })() + // }, []) - const onUpdateTodoStatus = (e: React.ChangeEvent, todoId: any) => { - dispatch(updateTodoStatus(todoId, e.target.checked)) - } + // Get todos at startup + useEffect(() => { + (async () => { + const getTodosList = JSON.parse(localStorage.getItem('todos') || ''); + const getTheme = JSON.parse(localStorage.getItem('darkMode') || 'false'); + if (getTodosList) { + dispatch(setTodos(getTodosList)); + } else { + dispatch(setTodos([])); + } + if (getTheme) { + setDarkMode(getTheme); + } + })(); + }, []); - const onToggleAllTodo = (e: React.ChangeEvent) => { - dispatch(toggleAllTodos(e.target.checked)) + // Saving to localStorage + useEffect(() => { + localStorage.setItem('todos', JSON.stringify(todos)); + localStorage.setItem('darkMode', JSON.stringify(darkMode)); + }, [todos, darkMode]); + + const onCreateTodo = async (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + if (task.length < 1) { + setError('Please input something!'); + return false; + } + const resp = await Service.createTodo(task); + dispatch(createTodo(resp)); + setTask(''); + setError(''); } + }; + + const onUpdateTodoStatus = ( + e: React.ChangeEvent, + todoId: any + ) => { + dispatch(updateTodoStatus(todoId, e.target.checked)); + }; + + const onDeleteAllTodo = () => { + dispatch(deleteAllTodos()); + }; - const onDeleteAllTodo = () => { - dispatch(deleteAllTodos()); + const onDeleteTodo = (id: string): void => { + dispatch(deleteTodo(id)); + }; + + const toggleEditText = (id: string): void => { + setEditTodoIdx(id); + setEditMode(true); + }; + + const onToggleDarkMode = (): void => { + setDarkMode(!darkMode); + }; + + const handleUpdateTodoContent = async ( + e: React.KeyboardEvent + ) => { + if (e.key === 'Enter') { + if (newTask.length < 1) { + setEditError('Please input something!'); + return false; + } + dispatch(updateTodoContent(editTodoIdx, newTask)); + setEditError(''); + setNewTask(''); + setEditMode(false); + } + if (e.key === 'Escape') { + setEditError(''); + setNewTask(''); + setEditMode(false); } + }; + const handleClickActive = (): void => { + dispatch(updateAllStatus(false)); + }; - return ( -
-
- -
-
- { - todos.map((todo, index) => { - return ( -
- onUpdateTodoStatus(e, index)} - /> - {todo.content} - -
- ); - }) - } -
-
- {todos.length > 0 ? - :
- } -
- - - + const handleClickCompleted = (): void => { + dispatch(updateAllStatus(true)); + }; + + return ( +
+
+ + {/* Notify error when leaving blank on input */} + {!!error && {error}} +
+
+ {todos.map((todo, index) => { + return ( +
+ onUpdateTodoStatus(e, todo.id)} + /> + {/* Adding some style to content to distinguish ACTIVE/COMPLETED tasks */} + {todo.id === editTodoIdx && editMode === true ? ( +
+ { + setEditError(''); + setNewTask(''); + setEditMode(false); + }} + /> + {!!editError && ( + {editError} + )}
- + ) : ( + toggleEditText(todo.id)} + > + {todo.content} + + )} +
+ ); + })} +
+
+ <> + + +
+ +
- ); + +
+
+ ); }; -export default ToDoPage; \ No newline at end of file +export default ToDoPage; diff --git a/src/components/Themes.js b/src/components/Themes.js new file mode 100644 index 000000000..5a0673004 --- /dev/null +++ b/src/components/Themes.js @@ -0,0 +1,11 @@ +export const DarkMode = { + text: '#FFF', + body: '#171D30', + backgroundColor: '#3E3F48', +}; + +export const LightMode = { + text: '#000', + body: '#E0E0E0', + backgroundColor: '#FFF', +}; diff --git a/src/store/actions.ts b/src/store/actions.ts index 59e59c200..8e4642358 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -6,6 +6,8 @@ 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' +export const UPDATE_ALL_STATUS = 'UPDATE_ALL_STATUS' export interface SetTodoAction { @@ -89,10 +91,44 @@ export function toggleAllTodos(checked: boolean): ToggleAllTodosAction { } } +/////////// +export interface UpdateTodoContentAction { + type: typeof UPDATE_TODO_CONTENT, + payload: { + todoId: string, + newTodo: string + } +} + +export function updateTodoContent(todoId: string, newTodo: string): UpdateTodoContentAction { + return { + type: UPDATE_TODO_CONTENT, + payload: { + todoId, + newTodo + } + } +} + +/////////// +export interface UpdateAllStatus { + type: typeof UPDATE_ALL_STATUS, + payload: boolean +} + +export function updateAllStatus(status: boolean): UpdateAllStatus { + return { + type: UPDATE_ALL_STATUS, + payload: status + } +} + export type AppActions = SetTodoAction | CreateTodoAction | UpdateTodoStatusAction | DeleteTodoAction | DeleteAllTodosAction | - ToggleAllTodosAction; \ No newline at end of file + ToggleAllTodosAction | + UpdateTodoContentAction | + UpdateAllStatus; \ No newline at end of file diff --git a/src/store/reducer.ts b/src/store/reducer.ts index a25f65859..3b1460128 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,11 +1,14 @@ import {Todo, TodoStatus} from '../models/todo'; import { AppActions, + SET_TODO, CREATE_TODO, DELETE_ALL_TODOS, DELETE_TODO, TOGGLE_ALL_TODOS, - UPDATE_TODO_STATUS + UPDATE_TODO_STATUS, + UPDATE_TODO_CONTENT, + UPDATE_ALL_STATUS } from './actions'; export interface AppState { @@ -13,24 +16,39 @@ export interface AppState { } export const initialState: AppState = { - todos: [] + todos: [], } function reducer(state: AppState, action: AppActions): AppState { switch (action.type) { case CREATE_TODO: - state.todos.push(action.payload); + // If using push here, due to immutability issue, React always recommends that NEVER mutate state directly. Treat states as if it were immutable + // Solution: clone that array + // const newTodosArray : Todo[] = [...state.todos] + // newTodosArray.push(action.payload); + // return { + // ...state, + // todos: newTodosArray + // }; + // OR in es6 as simple as this: return { - ...state + todos: [...state.todos, action.payload] }; + case SET_TODO: + return{ + todos: action.payload + } case UPDATE_TODO_STATUS: + // also immutability issues const index2 = state.todos.findIndex((todo) => todo.id === action.payload.todoId); - state.todos[index2].status = action.payload.checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE; + // state.todos[index2].status = action.payload.checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE; + const newTodosArray1 : Todo[] = [...state.todos]; + newTodosArray1[index2].status= action.payload.checked ? TodoStatus.COMPLETED : TodoStatus.ACTIVE; return { ...state, - todos: state.todos + todos: newTodosArray1 } case TOGGLE_ALL_TODOS: @@ -47,18 +65,44 @@ function reducer(state: AppState, action: AppActions): AppState { } case DELETE_TODO: - const index1 = state.todos.findIndex((todo) => todo.id === action.payload); - state.todos.splice(index1, 1); - + // same with CREATE_TODO problem, immutability issue. + // Solution: clone that array + // const newTodosArray: Todo[] = [...state.todos] + // const index1 = newTodosArray.findIndex((todo) => todo.id === action.payload); + // newTodosArray.splice(index1, 1); + // return { + // ...state, + // todos: newTodosArray + // } + // OR I find it more delicate using filter method instead: + const newTodosArray : Todo[] = state.todos.filter((todo) => todo.id !== action.payload); return { ...state, - todos: state.todos + todos: newTodosArray } case DELETE_ALL_TODOS: return { ...state, todos: [] } + // Adding edit mode for todo content: + case UPDATE_TODO_CONTENT: + const index3 = state.todos.findIndex((todo) => todo.id === action.payload.todoId); + const newTodosArray2 : Todo[] = [...state.todos]; + newTodosArray2[index3].content = action.payload.newTodo; + + return { + ...state, + todos: newTodosArray2 + } + + case UPDATE_ALL_STATUS: + const newTodosArray3 : Todo[] = [...state.todos] + newTodosArray3.map(todo => action.payload ? todo.status = TodoStatus.COMPLETED : todo.status = TodoStatus.ACTIVE) + return { + ...state, + todos: newTodosArray3 + } default: return state; }