From b6611f41f8196ff5114448c4a590aaf7697f15c0 Mon Sep 17 00:00:00 2001 From: Son-Nguyen2 Date: Tue, 17 May 2022 00:17:06 +0700 Subject: [PATCH] init base --- src/App.css | 14 ++- src/ToDoPage.tsx | 228 +++++++++++++++++++++-------------- src/models/todo.ts | 10 +- src/service/api-frontend.ts | 46 +++---- src/service/api-fullstack.ts | 29 +++-- src/service/index.ts | 10 +- src/service/types.ts | 8 +- src/store/actions.ts | 103 +++++++++------- src/store/reducer.ts | 107 +++++++++++----- src/utils/axios.ts | 16 +-- src/utils/constant.ts | 1 + src/utils/index.ts | 15 ++- 12 files changed, 359 insertions(+), 228 deletions(-) create mode 100644 src/utils/constant.ts diff --git a/src/App.css b/src/App.css index cc74a3fea..7d3de7983 100644 --- a/src/App.css +++ b/src/App.css @@ -7,7 +7,7 @@ button { outline: none; border: none; - box-shadow: 2px 0 2px currentColor; + box-shadow: 1px 2px 3px currentColor; border-radius: 4px; min-height: 32px; min-width: 80px; @@ -45,7 +45,7 @@ input:focus { box-shadow: 1px 0 9px rgba(0,0,0, 0.25); } -.ToDo__container { +.Todo__container { border: 1px solid rgba(0,0,0, 0.13); border-radius: 8px; width: 500px; @@ -63,19 +63,19 @@ input:focus { flex: 1 1; } -.ToDo__list { +.Todo__list { display: flex; flex-direction: column; margin-top: 1.5rem; } -.ToDo__item { +.Todo__item { display: flex; align-items: center; justify-content: space-between; } -.ToDo__item > span { +.Todo__item > span { flex: 1 1; text-align: left; margin-left: 8px; @@ -122,4 +122,8 @@ input:focus { } .Action__btn { +} + +.Todo__edit { + margin-right: 8px; } \ No newline at end of file diff --git a/src/ToDoPage.tsx b/src/ToDoPage.tsx index 1909718d0..dde1786a0 100644 --- a/src/ToDoPage.tsx +++ b/src/ToDoPage.tsx @@ -1,107 +1,157 @@ -import React, {useEffect, useReducer, useRef, useState} from 'react'; +import React, { useEffect, useMemo, 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, + toggleAllTodos, + deleteAllTodos, + updateTodoStatus, } from './store/actions'; import Service from './service'; -import {TodoStatus} from './models/todo'; +import { TodoStatus } from './models/todo'; +import { deleteTodo, updateTodoContent } from './store/actions'; +import { isTodoCompleted, getLocalStorage } from './utils/index'; 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 [{ todos }, dispatch] = useReducer(reducer, initialState); + const [showing, setShowing] = useState('ALL'); + const [todoEditId, setTodoEditId] = useState(''); + const inputRef = useRef(null); - const onUpdateTodoStatus = (e: React.ChangeEvent, todoId: any) => { - dispatch(updateTodoStatus(todoId, e.target.checked)) - } + const onCreateTodo = async (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && inputRef.current.value) { + const isDuplicate = todos.find( + (todo) => + todo.content.toLowerCase() === inputRef.current.value.toLowerCase() + ); + if (isDuplicate) { + alert('Duplicate task!'); + return; + } - const onToggleAllTodo = (e: React.ChangeEvent) => { - dispatch(toggleAllTodos(e.target.checked)) + if (todoEditId) { + dispatch(updateTodoContent(todoEditId, inputRef.current.value)); + setTodoEditId(''); + } else { + const resp = await Service.createTodo(inputRef.current.value); + dispatch(createTodo(resp)); + } + inputRef.current.value = ''; } + }; - const onDeleteAllTodo = () => { - dispatch(deleteAllTodos()); - } + const onUpdateTodoStatus = ( + e: React.ChangeEvent, + todoId: string + ) => { + dispatch(updateTodoStatus(todoId, e.target.checked)); + }; + const onToggleAllTodo = (e: React.ChangeEvent) => { + dispatch(toggleAllTodos(e.target.checked)); + }; - return ( -
-
- -
-
- { - todos.map((todo, index) => { - return ( -
- onUpdateTodoStatus(e, index)} - /> - {todo.content} - -
- ); - }) - } -
-
- {todos.length > 0 ? - :
- } -
- - - -
- + const onDeleteAllTodo = () => { + dispatch(deleteAllTodos()); + }; + + const deleteTodoById = (id: string) => { + dispatch(deleteTodo(id)); + }; + + const discardContent = () => { + inputRef.current.value = ''; + }; + + const editTodoById = (id: string) => { + const todo = todos.find((todo) => todo.id === id); + inputRef.current.value = todo?.content; + setTodoEditId(id); + }; + + const todosShowing = useMemo(() => { + return todos.filter((todo) => showing === 'ALL' || todo.status === showing); + }, [showing, todos]); + + useEffect(() => { + (async () => { + const todos = getLocalStorage(); + dispatch(setTodos(todos)); + })(); + }, []); + + return ( +
+
+ +
+
+ {todosShowing.map((todo, index) => { + return ( +
+ onUpdateTodoStatus(e, todo.id)} + /> + {todo.content} + +
+ ); + })} +
+
+ {todos.length > 0 ? ( + + ) : ( +
+ )} + +
+ + +
- ); + + +
+
+ ); }; -export default ToDoPage; \ No newline at end of file +export default ToDoPage; diff --git a/src/models/todo.ts b/src/models/todo.ts index f6ab9e951..5d64d02f6 100644 --- a/src/models/todo.ts +++ b/src/models/todo.ts @@ -1,8 +1,12 @@ export enum TodoStatus { ACTIVE = 'ACTIVE', - COMPLETED = 'COMPLETED' + COMPLETED = 'COMPLETED', } export interface Todo { - [key: string]: any -} \ No newline at end of file + id: string; + user_id: string; + content: string; + status: TodoStatus; + created_date: Object; +} diff --git a/src/service/api-frontend.ts b/src/service/api-frontend.ts index 205702c5f..772307deb 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 shortid from "shortid"; +import { IAPI } from './types'; +import { Todo, TodoStatus } from '../models/todo'; +import shortid from 'shortid'; 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..c68169813 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 { IAPI } from './types'; +import { Todo } from '../models/todo'; import axios from '../utils/axios'; -import {AxiosResponse} from 'axios'; +import { AxiosResponse } from 'axios'; 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..69202718b 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; +let Service: IAPI; if (process.env.REACT_APP_WHOAMI === 'frontend') { - Service = require('./api-frontend').default as IAPI + 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..69045d461 100644 --- a/src/service/types.ts +++ b/src/service/types.ts @@ -1,6 +1,6 @@ -import {Todo} from '../models/todo'; +import { Todo } from '../models/todo'; 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/store/actions.ts b/src/store/actions.ts index 59e59c200..27bba16a5 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -1,4 +1,4 @@ -import {Todo} from "../models/todo"; +import { Todo } from '../models/todo'; export const SET_TODO = 'SET_TODO'; export const CREATE_TODO = 'CREATE_TODO'; @@ -6,93 +6,106 @@ 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 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, +export interface UpdateTodoContentAction { + type: typeof UPDATE_TODO_CONTENT; payload: { - todoId: string, - checked: boolean - } + todoId: string; + content: string; + }; +} +export function updateTodoContent( + todoId: string, + content: string +): UpdateTodoContentAction { + return { + type: UPDATE_TODO_CONTENT, + payload: { + todoId, + content, + }, + }; } -export function updateTodoStatus(todoId: string, checked: boolean): UpdateTodoStatusAction { +export interface UpdateTodoStatusAction { + type: typeof UPDATE_TODO_STATUS; + payload: { + todoId: string; + checked: boolean; + }; +} +export function updateTodoStatus( + todoId: string, + checked: boolean +): UpdateTodoStatusAction { return { type: UPDATE_TODO_STATUS, payload: { todoId, - checked - } - } + checked, + }, + }; } -////////////// 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 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..c4b4db355 100644 --- a/src/store/reducer.ts +++ b/src/store/reducer.ts @@ -1,67 +1,116 @@ -import {Todo, TodoStatus} from '../models/todo'; +import { Todo, TodoStatus } from '../models/todo'; +import { setLocalStorage } from '../utils/index'; import { AppActions, CREATE_TODO, DELETE_ALL_TODOS, DELETE_TODO, TOGGLE_ALL_TODOS, - UPDATE_TODO_STATUS + SET_TODO, + UPDATE_TODO_STATUS, + UPDATE_TODO_CONTENT, } 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 SET_TODO: { + const todos = action.payload; + + setLocalStorage(todos); + + return { + ...state, + todos, + }; + } + case CREATE_TODO: { + const todos = [...state.todos, action.payload]; + + setLocalStorage(todos); + + return { + ...state, + todos, + }; + } + case UPDATE_TODO_STATUS: { + const todos = state.todos.map((todo) => { + if (todo.id === action.payload.todoId) { + todo.status = action.payload.checked + ? TodoStatus.COMPLETED + : TodoStatus.ACTIVE; + } + return todo; + }); + + setLocalStorage(todos); + return { - ...state + ...state, + todos, }; + } + case UPDATE_TODO_CONTENT: { + const todos = state.todos.map((todo) => { + if (todo.id === action.payload.todoId) { + todo.content = action.payload.content; + } + return todo; + }); - 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; + setLocalStorage(todos); return { ...state, - todos: state.todos - } + todos, + }; + } - case TOGGLE_ALL_TODOS: - const tempTodos = state.todos.map((e)=>{ + case TOGGLE_ALL_TODOS: { + const todos = state.todos.map((e) => { return { ...e, - status: action.payload ? TodoStatus.COMPLETED : TodoStatus.ACTIVE - } - }) + status: action.payload ? TodoStatus.COMPLETED : TodoStatus.ACTIVE, + }; + }); + + setLocalStorage(todos); return { ...state, - todos: tempTodos - } + todos, + }; + } + + case DELETE_TODO: { + const todos = state.todos.filter((todo) => todo.id !== action.payload); - case DELETE_TODO: - const index1 = state.todos.findIndex((todo) => todo.id === action.payload); - state.todos.splice(index1, 1); + setLocalStorage(todos); return { ...state, - todos: state.todos - } - case DELETE_ALL_TODOS: + todos, + }; + } + case DELETE_ALL_TODOS: { + setLocalStorage([]); + return { ...state, - todos: [] - } + todos: [], + }; + } default: return state; } } -export default reducer; \ No newline at end of file +export default reducer; diff --git a/src/utils/axios.ts b/src/utils/axios.ts index 14cf39dea..22070b968 100644 --- a/src/utils/axios.ts +++ b/src/utils/axios.ts @@ -1,14 +1,14 @@ 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/constant.ts b/src/utils/constant.ts new file mode 100644 index 000000000..b393f816b --- /dev/null +++ b/src/utils/constant.ts @@ -0,0 +1 @@ +export const TODOS_KEY = 'todos'; diff --git a/src/utils/index.ts b/src/utils/index.ts index bcb4b6d32..7d64d7f04 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,5 @@ -import {Todo, TodoStatus} from '../models/todo'; +import { Todo, TodoStatus } from '../models/todo'; +import { TODOS_KEY } from './constant'; export function isTodoCompleted(todo: Todo): boolean { return todo.status === TodoStatus.COMPLETED; @@ -6,4 +7,14 @@ export function isTodoCompleted(todo: Todo): boolean { export function isTodoActive(todo: Todo): boolean { return todo.status === TodoStatus.ACTIVE; -} \ No newline at end of file +} + +export function setLocalStorage(todo: Todo[]): void { + localStorage.setItem(TODOS_KEY, JSON.stringify(todo)); +} + +export function getLocalStorage(): Todo[] { + const todos = localStorage.getItem(TODOS_KEY); + if (!todos) return []; + return JSON.parse(todos); +}