Skip to content

Commit

Permalink
Merge pull request #6 from study-hex/f/todo
Browse files Browse the repository at this point in the history
F/todo
  • Loading branch information
Aya-X authored Sep 10, 2023
2 parents e3f1400 + 2f3da08 commit 6f67964
Show file tree
Hide file tree
Showing 15 changed files with 762 additions and 43 deletions.
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</head>

<body
class="font-noto text-dark bg-primary selection:text-primary selection:bg-dark">
class="font-noto text-dark bg-primary selection:text-primary selection:bg-dark overflow-y-hidden">
<div id="root"></div>

<script type="module" src="/src/main.tsx"></script>
Expand Down
26 changes: 26 additions & 0 deletions src/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Link
to="/signup"
className="block leading-[3rem] hover:opacity-80"
title="TODOLIST"
>
<h1 className={h1Class}>ONLINE TODO LIST</h1>
</Link>
);
}
// end of Logo

export default Logo;
2 changes: 1 addition & 1 deletion src/components/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
56 changes: 56 additions & 0 deletions src/components/TodoAddInput.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="mx-auto mb-4 flex max-w-[500px] items-center justify-between rounded-lg bg-white py-1 pl-4 pr-1 shadow">
<input
type="text"
name="content"
id="inputAddToto"
placeholder="新增待辦事項"
className="w-[83%] font-medium text-dark outline-none placeholder:text-light"
value={newInputTodo.value}
onChange={newInputTodo.onChange}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAddTodo(newInputTodo.value.trim());
return newInputTodo.clear();
}
if (e.key === 'Escape') {
return newInputTodo.clear();
}
}}
/>

<button
type="button"
className="transition-border relative h-10 w-10 rounded-[10px] bg-dark duration-100 hover:border-light disabled:bg-light"
disabled={!newInputTodo.value}
onClick={() => handleAddTodo(newInputTodo.value.trim())}
>
<p className="sr-only">ADD</p>
<span
className="absolute left-[45%] top-[20%] h-[60%] w-[12%] rounded-[10px] bg-white"
aria-hidden="true"
></span>
<span
className="absolute left-[45%] top-[20%] h-[60%] w-[12%] rotate-90 transform rounded-[10px] bg-white"
aria-hidden="true"
></span>
</button>
</div>
);
}
// end of TodoAddInput

export default TodoAddInput;
236 changes: 236 additions & 0 deletions src/components/TodoCardBody.tsx
Original file line number Diff line number Diff line change
@@ -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<ITodoData[]>([]);

const [isEditId, setIsEditId] = useState<string>('');
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 (
<div className="-mt-[2px] flex-1 border-t-[1px] pt-6">
<ul className="flex flex-col gap-4 px-4">
{!filterData.length && (
<li className="text-center text-xl">
<p>目前沒有資料 \(^Д^)/</p>
</li>
)}

{filterData &&
filterData.map((todo) => {
return (
<li key={todo.id}>
<div className="flex items-center gap-4 border-b-[1px] pb-4">
<label htmlFor="todoStatus">
<input
type="checkbox"
name="checkbox"
id="todoStatus"
aria-label="STATUS"
className="absolute h-5 w-5 cursor-pointer opacity-0"
value={`${todo.status}`}
checked={todo.status}
onChange={() => handleToggleTodo(todo)}
/>

<div className="custom-check flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-md border-[1px] border-light bg-white">
<svg
className="pointer-events-none hidden h-[18px] w-[18px] fill-current text-primary"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<g clipPath="url(#clip0_6_706)">
<path
d="M17.5817 2.94198C17.0248 2.38432 16.1204 2.38467 15.5628 2.94198L6.47614 12.029L2.43751 7.99039C1.87985 7.43272 0.975883 7.43272 0.418218 7.99039C-0.139447 8.54805 -0.139447 9.45202 0.418218 10.0097L5.46628 15.0577C5.74493 15.3364 6.11032 15.4761 6.47575 15.4761C6.84117 15.4761 7.20691 15.3367 7.48557 15.0577L17.5817 4.96124C18.1394 4.40396 18.1394 3.49961 17.5817 2.94198Z"
fill="#FFD370"
/>
</g>
<defs>
<clipPath id="clip0_6_706">
<rect width="18" height="18" fill="white" />
</clipPath>
</defs>
</svg>
</div>
{/* end of custom checkbox */}
</label>

{isEditId === todo.id ? (
<input
type="text"
name="content"
id="todoContent"
aria-label="todoContent"
className="relative flex-1 border-b-2 border-primary outline-none"
value={editInputTodo.value}
onChange={editInputTodo.onChange}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === 'Escape') {
handleEditTodo();
}
}}
/>
) : (
<button
type="button"
onClick={() => {
setIsEditId(todo.id);
editInputTodo.setValue(todo.content);
}}
className="flex-1 text-start"
>
<span
className={`${
todo.status && 'text-light line-through'
}`}
>
{todo.content}
</span>
</button>
)}

<button
type="button"
onClick={() => handleRemoveTodo(todo)}
className="hover:scale-125"
>
&times;
</button>
</div>
</li>
);
})}
</ul>
</div>
);
}

// end of TodoCardBody

export default TodoCardBody;
58 changes: 58 additions & 0 deletions src/components/TodoNav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="flex items-center justify-between gap-10 py-4">
<div className="w-4/5">
<Logo bgPosition={'bg-start'} />
</div>

<button type="button" className="text-sm" onClick={handleLogout}>
登出
</button>
</nav>
);
}

// end of TodoNav

export default TodoNav;
Loading

0 comments on commit 6f67964

Please sign in to comment.