Skip to content

Commit

Permalink
Refactors dashboard into smaller components
Browse files Browse the repository at this point in the history
  • Loading branch information
wdevon99 committed Mar 10, 2024
1 parent 1a55bfd commit 83fadf1
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 146 deletions.
File renamed without changes.
76 changes: 76 additions & 0 deletions src/components/molecules/CreateTodoModal/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client"

import { useState } from "react";
import { Form, Input, Modal, message } from "antd";
import TextArea from "antd/es/input/TextArea";
import TodoService from "@services/TodoService";

export default function CreateTodoModal({ isOpen, setIsOpen, todos, setTodos }: Props) {

const [form] = Form.useForm();
const [messageApi] = message.useMessage();
const [isBusy, setIsBusy] = useState(false);

const createTodo = async () => {
setIsBusy(true);

try {
const values = await form?.validateFields();
const { todoTitle, todoDescription } = values;

const response = await TodoService.createTodo(todoTitle, todoDescription);
const newTodo = await response.json();
const updatedTodos: any = [...(todos ?? []), newTodo];

setTodos(updatedTodos);
form?.resetFields();
setIsOpen(false)
messageApi.success("Added new todo");

} catch (error) {
console.error(error);
} finally {
setIsBusy(false);
}
};

return (
<Modal
title="Add new todo"
open={isOpen}
okText="Add"
okButtonProps={{ loading: isBusy }}
onOk={() => createTodo()}
onCancel={() => {
form?.resetFields();
setIsOpen(false)
}}
>
<p>What would you like to add to you todo list?</p>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="todoTitle"
rules={[{ required: true, message: 'Todo title is required!' }]}
>
<Input placeholder="Todo title" />
</Form.Item>
<Form.Item
name="todoDescription"
rules={[{ required: true, message: 'Todo description is required!' }]}
>
<TextArea rows={4} placeholder="Todo description" />
</Form.Item>
</Form>
</Modal>
);
}

type Props = {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
todos: Todo[];
setTodos: (todos: Todo[]) => void;
}
74 changes: 74 additions & 0 deletions src/components/molecules/TodosCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"use client"

import { Button, Card, List, Skeleton, message } from "antd";
import { PlusOutlined, DeleteOutlined, UndoOutlined } from '@ant-design/icons';
import TodoService from "@services/TodoService";
import styles from "./styles.module.sass";

export default function TodosCard({ todos, setTodos, setIsAddTodoModalOpen, isLoading }: Props) {
const CARD_WIDTH = 670;
const [messageApi, contextHolder] = message.useMessage();

const updateTodoStatus = async (todoId: string, isComplete: boolean) => {
const response = await TodoService.updateTodoStatus(todoId, !isComplete);
const updatedTodo: Todo = await response.json();
const updatedTodos = todos?.map((todo: Todo) => todo._id.toString() === updatedTodo._id.toString() ? updatedTodo : todo);

messageApi.success(updatedTodo.isComplete ? "Completed todo 🎉" : "Undo success");
setTodos(updatedTodos);
};

const deleteTodo = async (todoId: string) => {
const response = await TodoService.deleteTodo(todoId);
const deletedTodoId = await response.text();
const filteredTodos = todos?.filter((t: any) => t._id !== deletedTodoId);

messageApi.success("Deleted todo");
setTodos(filteredTodos);
};

const getListItemActions = (todo: Todo) => [
<Button type={todo.isComplete ? 'dashed' : 'dashed'} key="done" onClick={() => updateTodoStatus(todo._id, todo.isComplete)}>
{todo.isComplete ? <UndoOutlined /> : 'Complete'}
</Button>,
<Button type="dashed" danger key="delete" onClick={() => deleteTodo(todo._id)}>
<DeleteOutlined />
</Button>
]

return (
<section>
{contextHolder}
<Card
title={`Here are your todos`}
extra={<Button type="primary" size="small" shape="circle" onClick={() => setIsAddTodoModalOpen(true)}><PlusOutlined /></Button>}
style={{ width: CARD_WIDTH }}
>
<List
className={styles.list}
loading={isLoading}
itemLayout="horizontal"
loadMore={null}
dataSource={todos}
renderItem={(todo: Todo) => (
<List.Item actions={getListItemActions(todo)}>
<Skeleton title={false} loading={isLoading} active>
<List.Item.Meta
title={<span className={todo.isComplete ? styles.completedTodoText : ''}>{todo?.todoTitle}</span>}
description={<span className={todo.isComplete ? styles.completedTodoText : ''}>{todo?.todoDescription}</span>}
/>
</Skeleton>
</List.Item>
)}
/>
</Card>
</section>
);
}

type Props = {
todos: Todo[];
setTodos: (todos: Todo[]) => void;
setIsAddTodoModalOpen: (isOpen: boolean) => void;
isLoading: boolean;
}
2 changes: 2 additions & 0 deletions src/components/molecules/TodosCard/styles.module.sass
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.completedTodoText
text-decoration: line-through
30 changes: 30 additions & 0 deletions src/components/molecules/TodosProgressCard/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Card, Progress } from "antd";
import Colors from "@styles/variables.module.sass";

export default function TodosProgressCard({ todos }: Props) {
const CARD_WIDTH = 670;

const completedPercentage = () => {
if (!todos?.length) return 0;
const totalTodoCount = todos?.length || 0;
const completedTodoCount = todos?.filter((todo: Todo) => todo.isComplete)?.length || 0;

return Math.floor((completedTodoCount / totalTodoCount) * 100);
}

const getProgressBarColor = () => {
if (completedPercentage() === 100) return Colors.successColor;
if (completedPercentage() >= 50) return Colors.warningColor;
return Colors.errorColor;
}

return (
<Card style={{ width: CARD_WIDTH, marginTop: 20 }}>
<Progress percent={completedPercentage()} strokeColor={getProgressBarColor()} />
</Card>
);
}

type Props = {
todos: Todo[];
}
172 changes: 30 additions & 142 deletions src/components/pages/Dashboard/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
"use client"

import { useEffect, useState } from "react";
import { Button, Card, FloatButton, Form, Input, List, Modal, Progress, Skeleton, message } from "antd";
import { PlusOutlined, DeleteOutlined, ArrowLeftOutlined, UndoOutlined } from '@ant-design/icons';
import { FloatButton } from "antd";
import { ArrowLeftOutlined } from '@ant-design/icons';
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import TextArea from "antd/es/input/TextArea";
import TodoService from "@services/TodoService";
import CustomAvatar from "@components/atoms/CustomAvatar";
import Colors from "@styles/variables.module.sass";
import styles from "./styles.module.sass";
import TodosCard from "@components/molecules/TodosCard";
import CreateTodoModal from "@components/molecules/CreateTodoModal";
import TodosProgressCard from "@components/molecules/TodosProgressCard";

export default function Dashboard() {
const CARD_WIDTH = 670;

const router = useRouter();
const { data: session } = useSession();
const [messageApi, contextHolder] = message.useMessage();

const [form] = Form.useForm();
const [isBusy, setIsBusy] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [todos, setTodos] = useState<Todo[]>([]);
const [isAddTodoModalOpen, setIsAddTodoModalOpen] = useState(false);
Expand All @@ -41,142 +37,33 @@ export default function Dashboard() {
}
};

const createTodo = async () => {
setIsBusy(true);

try {
const values = await form?.validateFields();
const { todoTitle, todoDescription } = values;

const response = await TodoService.createTodo(todoTitle, todoDescription);
const newTodo = await response.json();
const updatedTodos: any = [...(todos ?? []), newTodo];

setTodos(updatedTodos);
form?.resetFields();
setIsAddTodoModalOpen(false)
messageApi.success("Added new todo");

} catch (error) {
console.error(error);
} finally {
setIsBusy(false);
}
};

const updateTodoStatus = async (todoId: string, isComplete: boolean) => {
const response = await TodoService.updateTodoStatus(todoId, !isComplete);
const updatedTodo: Todo = await response.json();
const updatedTodos = todos?.map((todo: Todo) => todo._id.toString() === updatedTodo._id.toString() ? updatedTodo : todo);

messageApi.success(updatedTodo.isComplete ? "Completed todo 🎉" : "Undo success");

setTodos(updatedTodos);
};

const deleteTodo = async (todoId: string) => {
const response = await TodoService.deleteTodo(todoId);
const deletedTodoId = await response.text();
const filteredTodos = todos?.filter((t: any) => t._id !== deletedTodoId);
messageApi.success("Deleted todo");

setTodos(filteredTodos);
};

const completedPercentage = () => {
if (!todos?.length) return 0;
const totalTodoCount = todos?.length;
const completedTodoCount = todos?.filter((todo: Todo) => todo.isComplete)?.length;

return Math.floor((completedTodoCount / totalTodoCount) * 100);
}

const getProgressBarColor = () => {
if (completedPercentage() === 100) return Colors.successColor;
if (completedPercentage() >= 50) return Colors.warningColor;
return Colors.errorColor;
}

const renderHeader = () => (
<>
return (
<main className={styles.container}>
<CustomAvatar
image={session?.user?.image}
image={session?.user?.image || ''}
size={60}
/>
<h1 className={styles.heading}>Hi {session?.user?.name?.split(' ')?.[0]} :)</h1>
<h2 className={styles.sub_heading}>Track your todos with ease.</h2>
</>
)

return (
<main className={styles.container}>
{contextHolder}
{renderHeader()}
<Card
title={`Here are your todos`}
extra={<Button type="primary" size="small" shape="circle" onClick={() => setIsAddTodoModalOpen(true)}><PlusOutlined /></Button>}
style={{ width: CARD_WIDTH }}
>
<List
className={styles.list}
loading={isLoading}
itemLayout="horizontal"
loadMore={null}
dataSource={todos}
renderItem={(todo: Todo) => (
<List.Item
actions={[
<Button type={todo.isComplete ? 'dashed' : 'dashed'} key="done" onClick={() => updateTodoStatus(todo._id, todo.isComplete)}>
{todo.isComplete ? <UndoOutlined /> : 'Complete'}
</Button>,
<Button type="dashed" danger key="delete" onClick={() => deleteTodo(todo._id)}>
<DeleteOutlined />
</Button>
]}
>
<Skeleton title={false} loading={isLoading} active>
<List.Item.Meta
title={<span className={todo.isComplete ? styles.completedTodoText : ''}>{todo?.todoTitle}</span>}
description={<span className={todo.isComplete ? styles.completedTodoText : ''}>{todo?.todoDescription}</span>}
/>
</Skeleton>
</List.Item>
)}
/>
</Card>
<Card style={{ width: CARD_WIDTH, marginTop: 20 }}>
<Progress percent={completedPercentage()} strokeColor={getProgressBarColor()} />
</Card>
<Modal
title="Add new todo"
open={isAddTodoModalOpen}
okText="Add"
okButtonProps={{ loading: isBusy }}
onOk={() => createTodo()}
onCancel={() => {
form?.resetFields();
setIsAddTodoModalOpen(false)
}}
>
<p>What would you like to add to you todo list?</p>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="todoTitle"
rules={[{ required: true, message: 'Todo title is required!' }]}
>
<Input placeholder="Todo title" />
</Form.Item>
<Form.Item
name="todoDescription"
rules={[{ required: true, message: 'Todo description is required!' }]}
>
<TextArea rows={4} placeholder="Todo description" />
</Form.Item>
</Form>
</Modal>
<h1 className={styles.heading}>
Hi {session?.user?.name?.split(' ')?.[0]} :)
</h1>
<h2 className={styles.sub_heading}>
Track your todos with ease.
</h2>
<TodosCard
todos={todos}
setTodos={setTodos}
setIsAddTodoModalOpen={setIsAddTodoModalOpen}
isLoading={isLoading}
/>
<TodosProgressCard
todos={todos}
/>
<CreateTodoModal
isOpen={isAddTodoModalOpen}
setIsOpen={setIsAddTodoModalOpen}
todos={todos}
setTodos={setTodos}
/>
<FloatButton
icon={<ArrowLeftOutlined />}
style={{ left: 30, top: 30 }}
Expand All @@ -185,3 +72,4 @@ export default function Dashboard() {
</main>
);
}

3 changes: 0 additions & 3 deletions src/components/pages/Dashboard/styles.module.sass
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,3 @@
font-size: 14px
font-weight: 300
margin-bottom: 40px

.completedTodoText
text-decoration: line-through
Loading

0 comments on commit 83fadf1

Please sign in to comment.