diff --git a/index.html b/index.html index 850b496..c09ca82 100644 --- a/index.html +++ b/index.html @@ -17,7 +17,7 @@ + class="font-noto text-dark bg-primary selection:text-primary selection:bg-dark overflow-y-hidden">
diff --git a/src/components/Logo.tsx b/src/components/Logo.tsx new file mode 100644 index 0000000..b40ce4f --- /dev/null +++ b/src/components/Logo.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +interface LogoProps { + bgPosition: string; +} +// end of interface + +function Logo({ bgPosition }: LogoProps): React.ReactElement { + const h1Class = `${bgPosition} + w-full overflow-hidden whitespace-nowrap bg-logo bg-[length:316px_46.9px] bg-no-repeat indent-[101%] + `; + + return ( + +

ONLINE TODO LIST

+ + ); +} +// end of Logo + +export default Logo; diff --git a/src/components/Toast.tsx b/src/components/Toast.tsx index a89a8a6..5e15af2 100644 --- a/src/components/Toast.tsx +++ b/src/components/Toast.tsx @@ -4,7 +4,7 @@ export const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, - timer: 3000, + timer: 1500, timerProgressBar: true, didOpen: (toast) => { toast.addEventListener('mouseenter', Swal.stopTimer); diff --git a/src/components/TodoAddInput.tsx b/src/components/TodoAddInput.tsx new file mode 100644 index 0000000..55bc81a --- /dev/null +++ b/src/components/TodoAddInput.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { useInput } from '../hooks/useInput'; + +interface ITodoAddInputProps { + handleAddTodo: (newInput: string) => void; +} +// end of interface + +function TodoAddInput(props: ITodoAddInputProps): React.ReactElement { + const newInputTodo = useInput(''); + const { handleAddTodo } = props; + + return ( +
+ { + if (e.key === 'Enter') { + handleAddTodo(newInputTodo.value.trim()); + return newInputTodo.clear(); + } + if (e.key === 'Escape') { + return newInputTodo.clear(); + } + }} + /> + + +
+ ); +} +// end of TodoAddInput + +export default TodoAddInput; diff --git a/src/components/TodoCardBody.tsx b/src/components/TodoCardBody.tsx new file mode 100644 index 0000000..6299e81 --- /dev/null +++ b/src/components/TodoCardBody.tsx @@ -0,0 +1,236 @@ +import React, { useEffect, useState } from 'react'; + +import { api } from '../helpers/api'; +import { Toast } from './Toast'; + +import { useInput } from '../hooks/useInput'; + +interface ITodoData { + content: string; + createTime: number; + id: string; + status: boolean; +} + +interface ITodoCardBodyProps { + todoData: ITodoData[]; + getTodos: () => void; + isClickTab: string; + setIsClickTab: (isClickTab: string) => void; +} +// end of interface + +function TodoCardBody(props: ITodoCardBodyProps): React.ReactElement { + const { todoData, getTodos, isClickTab, setIsClickTab } = props; + + const [filterData, setFilterData] = useState([]); + + const [isEditId, setIsEditId] = useState(''); + const editInputTodo = useInput(''); + + const handleRemoveTodo = (todo: ITodoData) => { + api.deleteTodo(todo.id).then((res) => { + if (!res?.status) { + Toast.fire({ + icon: 'warning', + title: '請重新操作', + }); + } + // end of !res?.status + + if (res?.status) { + const msg = res.message || '刪除成功'; + + Toast.fire({ + icon: 'success', + title: msg, + }); + + getTodos(); + } + // end of res?.status + }); + // end of api + }; + // end of handleRemoveTodo + + const handleToggleTodo = (todo: ITodoData) => { + api.patchTodo(todo.id).then((res) => { + if (!res?.status) { + Toast.fire({ + icon: 'warning', + title: '請重新操作', + }); + } + // end of !res?.status + + if (res?.status) { + const msg = res.message || '編輯成功'; + + Toast.fire({ + icon: 'success', + title: msg, + }); + + getTodos(); + } + // end of res?.status + }); + // end of api + }; + // end of handleToggleTodo + + const handleEditTodo = () => { + const data = { + content: editInputTodo.value, + }; + + api.putTodo(isEditId, data).then((res) => { + if (!res?.status) { + Toast.fire({ + icon: 'warning', + title: '請重新操作', + }); + } + // end of !res?.status + + if (res?.status) { + const msg = res.message || '編輯成功'; + + Toast.fire({ + icon: 'success', + title: msg, + }); + + getTodos(); + } + // end of res?.status + + setIsEditId(''); + }); + // end of api + }; + // end of handleEditTodo + + useEffect(() => { + setFilterData( + [...todoData] + .filter((item) => { + if (isClickTab === 'ALL') { + return true; + } + if (isClickTab === 'TODO') { + return !item.status; + } + if (isClickTab === 'DONE') { + return item.status; + } + }) + .reverse(), + ); + }, [isClickTab, todoData]); + + return ( +
+
    + {!filterData.length && ( +
  • +

    目前沒有資料 \(^Д^)/

    +
  • + )} + + {filterData && + filterData.map((todo) => { + return ( +
  • +
    + + + {isEditId === todo.id ? ( + { + if (e.key === 'Enter' || e.key === 'Escape') { + handleEditTodo(); + } + }} + /> + ) : ( + + )} + + +
    +
  • + ); + })} +
+
+ ); +} + +// end of TodoCardBody + +export default TodoCardBody; diff --git a/src/components/TodoNav.tsx b/src/components/TodoNav.tsx new file mode 100644 index 0000000..a9fb8e3 --- /dev/null +++ b/src/components/TodoNav.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { api } from '../helpers/api'; +import { Toast } from '../components/Toast'; + +import Logo from './Logo'; + +function TodoNav(): React.ReactElement { + const navigate = useNavigate(); + + const handleLogout = () => { + api.logout().then((res: any) => { + if (!res?.status) { + Toast.fire({ + icon: 'warning', + title: '請重新操作', + }); + } + // end of !res?.status + + if (res?.status) { + const msg = res.message || '登出成功'; + + api.req.defaults.headers.common['Authorization'] = ''; + + Toast.fire({ + icon: 'success', + title: msg, + didClose: () => { + setTimeout(() => { + navigate('/login'); + }, 400); + }, + }); + } + // end of res?.status + }); + // end of api + }; + // end of handleLogout + + return ( + + ); +} + +// end of TodoNav + +export default TodoNav; diff --git a/src/components/TodoTabs.tsx b/src/components/TodoTabs.tsx new file mode 100644 index 0000000..568e873 --- /dev/null +++ b/src/components/TodoTabs.tsx @@ -0,0 +1,61 @@ +import React from 'react'; + +interface ITodoTabsProps { + isClickTab: string; + setIsClickTab: (isClickTab: string) => void; +} +// end of interface + +function TodoTabs(props: ITodoTabsProps): React.ReactElement { + const { isClickTab, setIsClickTab } = props; + + return ( +
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
+
+ ); +} + +// end of TodoTabs + +export default TodoTabs; diff --git a/src/helpers/api.tsx b/src/helpers/api.tsx index 44f8476..bb5555b 100644 --- a/src/helpers/api.tsx +++ b/src/helpers/api.tsx @@ -54,6 +54,24 @@ const check = async (cookieValue: string): Promise => { req.defaults.headers.common['Authorization'] = cookieValue; const res: AxiosResponse = await req.get('/users/checkout'); + + if (res?.status !== 200) { + throw new Error(); + } + + return res?.data; + } catch (error: any) { + if (error?.status === 400) { + return; + } + + handleError(error); + } +}; + +const logout = async (): Promise => { + try { + const res: AxiosResponse = await req.post('/users/sign_out'); if (res?.status !== 200) { throw new Error(); } @@ -64,4 +82,81 @@ const check = async (cookieValue: string): Promise => { } }; -export const api = { signup, login, check }; +const getTodo = async (): Promise => { + try { + const res: AxiosResponse = await req.get('/todos'); + if (res?.status !== 200) { + throw new Error(); + } + + return res?.data; + } catch (error: unknown) { + handleError(error); + } +}; + +const postTodo = async (data: object): Promise => { + try { + const res: AxiosResponse = await req.post('/todos', data); + if (res?.status !== 201) { + throw new Error(); + } + + return res?.data; + } catch (error: unknown) { + handleError(error); + } +}; + +const deleteTodo = async (todoId: string): Promise => { + try { + const res: AxiosResponse = await req.delete(`/todos/${todoId}`); + if (res?.status !== 200) { + throw new Error(); + } + + return res?.data; + } catch (error: unknown) { + handleError(error); + } +}; + +const patchTodo = async (todoId: string): Promise => { + try { + const res: AxiosResponse = await req.patch(`/todos/${todoId}/toggle`); + if (res?.status !== 200) { + throw new Error(); + } + + return res?.data; + } catch (error: unknown) { + handleError(error); + } +}; + +const putTodo = async (todoId: string, data: object): Promise => { + try { + const res: AxiosResponse = await req.put(`/todos/${todoId}`, data); + if (res?.status !== 200) { + throw new Error(); + } + + return res?.data; + } catch (error: unknown) { + handleError(error); + } +}; + +export const api = { + req, + handleError, + signup, + login, + check, + logout, + getTodo, + postTodo, + deleteTodo, + patchTodo, + putTodo, +}; diff --git a/src/hooks/useInput.tsx b/src/hooks/useInput.tsx new file mode 100644 index 0000000..3eff953 --- /dev/null +++ b/src/hooks/useInput.tsx @@ -0,0 +1,12 @@ +import { useState } from 'react'; + +export const useInput = (initValue: string) => { + const [value, setValue] = useState(initValue); + + const handleChange = (event: any) => { + setValue(event.target.value); + }; + + return { value, setValue, onChange: handleChange, clear: () => setValue('') }; +}; +// end of useInput() diff --git a/src/index.css b/src/index.css index 7ded3b6..2850632 100644 --- a/src/index.css +++ b/src/index.css @@ -3,5 +3,13 @@ @tailwind utilities; * { - outline: 1px solid #f73; + /* outline: 1px solid #f73; */ +} + +input:checked+.custom-check { + @apply border-0; +} + +input:checked+.custom-check svg { + @apply block; } \ No newline at end of file diff --git a/src/routes/Login.tsx b/src/routes/Login.tsx index b7b0b60..206af55 100644 --- a/src/routes/Login.tsx +++ b/src/routes/Login.tsx @@ -45,6 +45,8 @@ function Login(): React.ReactElement { // end of !res?.status if (res?.status) { + api.req.defaults.headers.common['Authorization'] = res?.token; + Toast.fire({ icon: 'success', title: '登入成功', @@ -106,7 +108,7 @@ function Login(): React.ReactElement { - +
+
+ + + + + {!todoData.length ? ( +
+

目前尚無待辦事項 (≥o≤)

+ + EMPTY +
+ ) : ( +
+ + + + + +
+ )} +
); } diff --git a/tailwind.config.js b/tailwind.config.js index 144837a..9bb7927 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,17 +1,18 @@ module.exports = { content: ['index.html', './src/**/*.{js,jsx,ts,tsx,vue,html}'], theme: { - colors: { - primary: '#FFD370', - dark: '#333333', - light: '#9F9A91', - }, - fontFamily: { - noto: ['Noto Sans TC', 'sans-serif'], - }, extend: { + fontFamily: { + noto: ['Noto Sans TC', 'sans-serif'], + }, + colors: { + primary: '#FFD370', + dark: '#333333', + light: '#9F9A91', + }, backgroundImage: { logo: "url('/src/images/logo.svg')", + linear: 'linear-gradient(173deg, #FFD370 5.12%, #FFD370 53.33%, #FFD370 53.44%, #FFF 53.45%, #FFF 94.32%)', }, }, },