Skip to content

Commit

Permalink
feat(client): added tasks list page
Browse files Browse the repository at this point in the history
  • Loading branch information
EchoSkorJjj committed Jan 7, 2024
1 parent 2478152 commit a14264b
Show file tree
Hide file tree
Showing 18 changed files with 671 additions and 1 deletion.
63 changes: 63 additions & 0 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"react-apexcharts": "^1.4.1",
"react-calendar": "^4.7.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.49.2",
"react-i18next": "^13.5.0",
Expand All @@ -41,6 +42,7 @@
"react-query": "^3.39.3",
"react-router-dom": "^6.20.0",
"react-table": "^7.8.0",
"react-textarea-autosize": "^8.5.3",
"usehooks-ts": "^2.9.1",
"uuid": "^9.0.1",
"zustand": "^4.4.7"
Expand Down
10 changes: 9 additions & 1 deletion client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useAuth } from "~features/auth";
// Private Component
const DashboardPage = lazy(() => import("~features/pages/dashboard/Dashboard"));
const PostsPage = lazy(() => import("~features/pages/posts/Posts"));
const TaskListPage = lazy(() => import("~features/pages/task/TaskList"));

// Public Component
const LandingPage = lazy(() => import("~features/pages/landing/Landing"));
Expand All @@ -29,6 +30,7 @@ const Sidebar = lazy(() => import("~shared/components/sidebar/Sidebar"));
const Navbar = lazy(() => import("~shared/components/navbar/Navbar"));
const Footer = lazy(() => import("~shared/components/footer/Footer"));
const Loader = lazy(() => import("~shared/components/loader/Loader"));
const NotFound = lazy(() => import("~shared/components/notfound/NotFound"));

const App: React.FC = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
Expand Down Expand Up @@ -98,14 +100,20 @@ const App: React.FC = () => {
<Route path="/signin" element={<LoginPage />} />
<Route path="/signup" element={<RegisterPage />} />
</Route>

{/* This is private route, only authenticated user can access this route */}
<Route element={<PrivateRoute resourceRequested="dashboard" />}>
<Route path="/dashboard" element={<DashboardPage />} />
</Route>
{/* <Route element={<PrivateRoute resourceRequested="tasklist" />}>
<Route path="/tasklist" element={<TaskListPage />} />
</Route> */}
<Route path="/tasklist" element={<TaskListPage />} />
{/* /post/* means that all paths starting with /post/ will be handled by PostPage. */}
<Route path="/posts/*" element={<PostsPage />} />

{/* This is 404 page, if no route match, this will be rendered */}
<Route path="*" element={<div>404</div>} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</Box>
Expand Down
29 changes: 29 additions & 0 deletions client/src/features/hooks/useColumnDrop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { useDrop } from "react-dnd";

import { ColumnType, DragItem, ItemType, TaskModel } from "~utils";

function useColumnDrop(
column: ColumnType,
handleDrop: (fromColumn: ColumnType, taskId: TaskModel["id"]) => void,
) {
const [{ isOver }, dropRef] = useDrop<DragItem, void, { isOver: boolean }>({
accept: ItemType.TASK,
drop: (dragItem) => {
if (!dragItem || dragItem.from === column) {
return;
}

handleDrop(dragItem.from, dragItem.id);
},
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
});

return {
isOver,
dropRef,
};
}

export default useColumnDrop;
156 changes: 156 additions & 0 deletions client/src/features/hooks/useColumnTasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { useCallback } from "react";
import { v4 as uuidv4 } from "uuid";

import { ColumnType, debug, TaskModel } from "~utils";

import useTaskCollection from "./useTaskCollection";

const MAX_TASK_PER_COLUMN = 100;

function swap<T>(arr: T[], num1: number, num2: number): T[] {
const copy = [...arr];
const tmp = copy[num1];
copy[num1] = copy[num2];
copy[num2] = tmp;
return copy;
}

function useColumnTasks(column: ColumnType) {
const [tasks, setTasks] = useTaskCollection();

const columnTasks = tasks[column];

const getColumnColor = (columnName: ColumnType): string => {
const colorMap: Record<ColumnType, string> = {
Blocked: "red.300",
Completed: "green.300",
"In Progress": "blue.300",
Todo: "gray.300",
};

return colorMap[columnName] || "blue.300";
};

const addEmptyTask = useCallback(() => {
debug(`Adding new empty task to ${column} column`);
setTasks((allTasks) => {
const columnTasks = allTasks[column];

if (columnTasks.length > MAX_TASK_PER_COLUMN) {
debug("Too many task!");
return allTasks;
}

const newColumnTask: TaskModel = {
id: uuidv4(),
title: `New ${column} task`,
color: getColumnColor(column),
column: column,
};

return {
...allTasks,
[column]: [newColumnTask, ...columnTasks],
};
});
}, [column, setTasks]);

const deleteTask = useCallback(
(id: TaskModel["id"]) => {
debug(`Removing task ${id}..`);
setTasks((allTasks) => {
const columnTasks = allTasks[column];
return {
...allTasks,
[column]: columnTasks.filter((task) => task.id !== id),
};
});
},
[column, setTasks],
);

const updateTask = useCallback(
(id: TaskModel["id"], updatedTask: Omit<Partial<TaskModel>, "id">) => {
debug(`Updating task ${id} with ${JSON.stringify(updateTask)}`);
setTasks((allTasks) => {
const columnTasks = allTasks[column];
return {
...allTasks,
[column]: columnTasks.map((task) =>
task.id === id ? { ...task, ...updatedTask } : task,
),
};
});
},
[column, setTasks],
);

const dropTaskFrom = useCallback(
(from: ColumnType, id: TaskModel["id"]) => {
setTasks((allTasks) => {
const fromColumnTasks = allTasks[from];
const toColumnTasks = allTasks[column];
const movingTask = fromColumnTasks.find((task) => task.id === id);

debug(`Moving task ${movingTask?.id} from ${from} to ${column}`);

if (!movingTask) {
return allTasks;
}

const updatedMovingTask = {
...movingTask,
column,
color: getColumnColor(column),
};

return {
...allTasks,
[from]: fromColumnTasks.filter((task) => task.id !== id),
[column]: [updatedMovingTask, ...toColumnTasks],
};
});
},
[column, setTasks],
);

const swapTasks = useCallback(
(num1: number, num2: number) => {
debug(`Swapping task ${num1} with ${num2} in ${column} column`);
setTasks((allTasks) => {
let columnTasks = allTasks[column];

columnTasks = columnTasks.map((task, index) => {
if (index === num1 || index === num2) {
return { ...task, color: getColumnColor(column) };
}
return task;
});

columnTasks = swap(columnTasks, num1, num2).map((task, index) => {
if (index === num1 || index === num2) {
return { ...task, color: getColumnColor(column) };
}
return task;
});

return {
...allTasks,
[column]: columnTasks,
};
});
},
[column, setTasks, getColumnColor],
);

return {
tasks: columnTasks,
addEmptyTask,
updateTask,
dropTaskFrom,
deleteTask,
swapTasks,
};
}

export default useColumnTasks;
45 changes: 45 additions & 0 deletions client/src/features/hooks/useTaskCollection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useLocalStorage } from "usehooks-ts";
import { v4 as uuidv4 } from "uuid";

import { ColumnType, TaskModel } from "~utils";

function useTaskCollection() {
return useLocalStorage<{
[key in ColumnType]: TaskModel[];
}>("tasks", {
Todo: [
{
id: uuidv4(),
column: ColumnType.TO_DO,
title: "Task 1",
color: "grey.300",
},
],
"In Progress": [
{
id: uuidv4(),
column: ColumnType.IN_PROGRESS,
title: "Task 2",
color: "blue.300",
},
],
Blocked: [
{
id: uuidv4(),
column: ColumnType.BLOCKED,
title: "Task 3",
color: "red.300",
},
],
Completed: [
{
id: uuidv4(),
column: ColumnType.COMPLETED,
title: "Task 4",
color: "green.300",
},
],
});
}

export default useTaskCollection;
Loading

0 comments on commit a14264b

Please sign in to comment.