diff --git a/index.html b/index.html
index f7ac4e4..75b2656 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,10 @@
-
Todo
+ Task manager
+
+
+
diff --git a/package.json b/package.json
index caf6289..669c1e7 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,8 @@
},
"dependencies": {
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-dom": "^19.0.0",
+ "zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..3c28a9b
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,353 @@
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ font-family: "Inter", sans-serif;
+ font-optical-sizing: auto;
+}
+
+body {
+ background-color: #fffbb0;
+ color: #1d1d1d;
+ display: flex;
+ justify-content: center;
+ min-height: 100vh;
+ padding: 20px;
+ max-width: 750px;
+ margin: 0 auto;
+ }
+
+ h1 {
+ font-family: "Bebas Neue", sans-serif;
+ font-weight: 400;
+ font-size: 72px;
+ color: #a0074c;
+ text-align: center;
+ margin-bottom: 20px;
+ }
+
+ p {
+ font-size: 16px;
+ }
+
+
+ .todo-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ margin-bottom: 20px;
+ }
+
+ .dueDate-submit-container {
+ display: flex;
+ gap: 20px;
+ }
+
+ .todo-form input[type="text"] {
+ flex-grow: 1;
+ word-break: break-word;
+ padding: 8px;
+ border: 1px solid #a0074c;
+ background-color: white;
+ border-radius: 4px;
+ font-size: 16px;
+ transition: border-color 0.3s ease;
+ }
+
+ .todo-form input[type="text"]:focus {
+ border-color: #a0074c;
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(160, 7, 76, 0.2);
+}
+
+ .todo-form input[type="date"] {
+ padding: 8px;
+ border: 1px solid #a0074c;
+ background-color: transparent;
+ border-radius: 4px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: border-color 0.3s ease;
+ }
+
+ .todo-form input[type="date"]:focus {
+ border-color: #a0074c;
+ outline: none;
+ box-shadow: 0 0 0 3px rgba(160, 7, 76, 0.2);
+}
+
+ .todo-form button {
+ font-family: "Inter", sans-serif;
+ padding: 8px;
+ border: 1px solid #a0074c;
+ background-color: transparent;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ transition: background-color 0.2s ease;
+ }
+
+ .todo-form button:hover {
+ background-color: #a0074c;
+ color: #fffbb0;
+ }
+
+ .task-count {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ gap: 20px;
+ margin-bottom: 20px;
+ color: #a0074c;
+ }
+
+ .task-count p {
+ margin: 5px 0;
+ }
+
+ .todo-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .task-main-content {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ margin-bottom: 20px;
+ gap: 20px;
+ }
+
+ .todo-item {
+ display: flex;
+ justify-content: space-evenly;
+ flex-direction: column;
+ padding: 20px;
+ border-bottom: 1px solid #a0074c;
+ transition: background-color 0.2s ease;
+ }
+
+ .todo-item.completed {
+ background-color: #e6ffe6;
+ }
+
+ .todo-item input[type="checkbox"] {
+ transform: scale(1.3);
+ cursor: pointer;
+ accent-color: #a0074c;
+ border-radius: 4px;
+ transition: all 0.2s ease;
+ }
+
+ .todo-item input[type="checkbox"]:focus {
+ outline: 1px solid #a0074c;
+ outline-offset: 1px;
+}
+
+ .todo-item .task-text {
+ flex-grow: 1;
+ font-size: 16px;
+ word-break: break-word;
+ }
+
+ .todo-item.completed .task-text {
+ text-decoration: line-through;
+ color: #888;
+ }
+
+ .todo-item button {
+ background-color: transparent;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 16px;
+ transition: background-color 0.2s ease;
+ }
+
+ .todo-item button span {
+ border-bottom: 1px solid #a0074c;
+ }
+
+
+ .todo-item button span:hover {
+ border-bottom: 1px solid #1d1d1d;
+ }
+
+ .task-meta {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: 20px;
+ font-size: 16px;
+ }
+
+ .todo-item.overdue {
+ border-left: 5px solid #f80000;
+ }
+
+ .overdue-text {
+ color: #f80000;
+ }
+
+ .task-filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 20px;
+ justify-content: space-between;
+ border-radius: 8px;
+ }
+
+ .task-filters button {
+ padding: 8px;
+ border: 1px solid #a0074c;
+ background-color: transparent;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background-color 0.2s ease, transform 0.1s ease;
+ flex-shrink: 1;
+ }
+
+ .task-filters button:hover {
+ background-color: #a0074c;
+ color: #fffbb0;
+ transform: translateY(-1px);
+ }
+
+ .task-filters button.active {
+ background-color: #a0074c;
+ color: #fffbb0;
+ font-weight: bold;
+ }
+
+ .empty-state {
+ text-align: center;
+ color: #1d1d1d;
+ padding: 20px;
+ border: 2px dashed #a0074c;
+ border-radius: 4px;
+ margin-top: 20px;
+ font-style: italic;
+ }
+
+ /* Responsive Design */
+ @media (max-width: 768px) {
+ .todo-form {
+ flex-direction: column;
+ }
+
+ .todo-form button {
+ width: 100%;
+ }
+
+ .todo-item {
+ flex-wrap: wrap;
+ }
+
+ .todo-item .task-text {
+ flex-basis: 100%;
+ margin-bottom: 10px;
+ }
+
+ .todo-item button {
+ margin-left: 0;
+ margin-top: 10px;
+ width: 100%;
+ }
+
+ .todo-item {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .task-main-content {
+ margin-bottom: 10px;
+ }
+
+ .task-meta {
+ margin-left: 30px;
+ margin-bottom: 10px;
+ }
+
+ .todo-item button {
+ position: static;
+ width: auto;
+ margin-top: 10px;
+ align-self: flex-end;
+ }
+
+ .task-filters {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: stretch;
+ gap: 8px;
+ margin-bottom: 20px;
+ border-radius: 8px;
+ }
+ }
+
+ @media (max-width: 480px) {
+
+ body {
+ padding: 10px;
+ }
+
+ h1 {
+ font-size: 42px;
+ }
+
+ p {
+ font-size: 12px;
+ }
+
+ .todo-form input,
+ .todo-form button {
+ font-size: 12px;
+ }
+
+ .todo-item {
+ padding: 10px;
+ max-width: 100%;
+ }
+
+ .todo-item .task-text {
+ font-size: 12px;
+ }
+
+ .task-filters {
+ flex-direction: column;
+ align-items: stretch;
+ }
+
+ .task-filters button {
+ width: 100%;
+ font-size: 12px;
+ }
+
+ .todo-item button {
+ width: 100%;
+ margin-left: 0;
+ }
+
+ .todo-item button {
+ font-size: 12px;
+ }
+
+ .todo-form input[type="text"] {
+ font-size: 12px;
+ }
+
+ .todo-form input[type="date"] {
+ font-size: 12px;
+ }
+
+ .task-meta {
+ font-size: 12px;
+ }
+ }
diff --git a/src/App.jsx b/src/App.jsx
index 5427540..50df68d 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,5 +1,18 @@
+import { Header } from './components/Header'
+import { TodoForm } from './components/TodoForm'
+import { TaskCount } from './components/TaskCount'
+import { TaskFilter } from './components/TaskFilter'
+import { TodoList } from './components/TodoList'
+import './App.css'
+
export const App = () => {
return (
- React Boilerplate
+ <>
+
+
+
+
+
+ >
)
}
diff --git a/src/components/Header.jsx b/src/components/Header.jsx
new file mode 100644
index 0000000..66ad8f8
--- /dev/null
+++ b/src/components/Header.jsx
@@ -0,0 +1,6 @@
+export const Header = () => {
+
+ return (
+ Task manager
+ )
+}
\ No newline at end of file
diff --git a/src/components/TaskCount.jsx b/src/components/TaskCount.jsx
new file mode 100644
index 0000000..b81b648
--- /dev/null
+++ b/src/components/TaskCount.jsx
@@ -0,0 +1,15 @@
+import { useTodoStore } from '../stores/useTodoStore'
+
+export const TaskCount = () => {
+ const tasks = useTodoStore((state) => state.tasks)
+ const completedTasks = tasks.filter(task => task.completed).length
+ const uncompletedTasks = tasks.filter(task => !task.completed).length
+
+ return (
+
+
Total Tasks: {tasks.length}
+
Completed: {completedTasks}
+
Uncompleted: {uncompletedTasks}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/TaskFilter.jsx b/src/components/TaskFilter.jsx
new file mode 100644
index 0000000..3de53d2
--- /dev/null
+++ b/src/components/TaskFilter.jsx
@@ -0,0 +1,30 @@
+import { useTodoStore } from '../stores/useTodoStore'
+
+export const TaskFilter = () => {
+ const setFilter = useTodoStore((state) => state.setFilter)
+ const currentFilter = useTodoStore((state) => state.currentFilter)
+
+ const filterButtons = [
+ { label: 'All', value: 'all' },
+ { label: 'Newest First', value: 'newest' },
+ { label: 'Oldest First', value: 'oldest' },
+ { label: 'Closest Deadline', value: 'due_date' },
+ { label: 'Completed', value: 'completed' },
+ { label: 'Uncompleted', value: 'uncompleted' },
+ ]
+
+ return (
+
+ {filterButtons.map((button) => (
+
+ ))}
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx
new file mode 100644
index 0000000..739464f
--- /dev/null
+++ b/src/components/TodoForm.jsx
@@ -0,0 +1,38 @@
+import { useState } from 'react'
+import { useTodoStore } from '../stores/useTodoStore'
+
+export const TodoForm = () => {
+ const [taskText, setTaskText] = useState('')
+ const [dueDate, setDueDate] = useState('')
+ const addTask = useTodoStore((state) => state.addTask)
+
+ const handleSubmit = (e) => {
+ e.preventDefault()
+ if (taskText.trim()) {
+ addTask(taskText, dueDate)
+ setTaskText('')
+ setDueDate('')
+ }
+ }
+
+ return (
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx
new file mode 100644
index 0000000..636c374
--- /dev/null
+++ b/src/components/TodoItem.jsx
@@ -0,0 +1,48 @@
+import { useTodoStore } from '../stores/useTodoStore'
+
+export const TodoItem = ({ task }) => {
+ const toggleTask = useTodoStore((state) => state.toggleTask)
+ const removeTask = useTodoStore((state) => state.removeTask)
+
+ const formatDate = (dateString) => {
+ if (!dateString) return ''
+ const date = new Date (dateString)
+ return date.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ })
+ }
+
+ const isOverDue = task.dueDate && !task.completed && new Date(task.dueDate) < new Date ()
+
+ return (
+
+
+ toggleTask(task.id)}
+ aria-label={`Mark ${task.text} as ${task.completed ? 'uncompleted' : 'completed'}`}
+ />
+ {task.text}
+
+
+
+
+ {task.createdAt && (
+
+ Added: {formatDate(task.createdAt)}
+
+ )}
+ {task.dueDate && (
+
+ Due: {formatDate(task.dueDate)}
+
+ )}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx
new file mode 100644
index 0000000..2b5dfe0
--- /dev/null
+++ b/src/components/TodoList.jsx
@@ -0,0 +1,61 @@
+import { useTodoStore } from '../stores/useTodoStore'
+import { TodoItem } from './TodoItem'
+
+export const TodoList = () => {
+ const tasks = useTodoStore((state) => state.tasks)
+ const currentFilter = useTodoStore((state) => state.currentFilter)
+
+ const filteredAndSortedTasks = tasks
+ .filter((task) => {
+ if (currentFilter === 'completed') {
+ return task.completed
+ }
+ if (currentFilter === 'uncompleted') {
+ return !task.completed
+ }
+ return true
+ })
+
+ .sort((a, b) => {
+ if (currentFilter === 'newest') {
+ return new Date(b.createdAt) - new Date(a.createdAt)
+ }
+ if (currentFilter === 'oldest') {
+ return new Date(a.createdAt) - new Date(b.createdAt)
+ }
+ if (currentFilter === 'due_date') {
+ if (a.dueDate && b.dueDate) {
+ return new Date(a.dueDate) - new Date(b.dueDate)
+ }
+ if (a.dueDate) return -1
+ if (b.dueDate) return 1
+ return 0
+ }
+ return 0
+ })
+
+
+ if (tasks.length === 0) {
+ return (
+
+ No tasks yet! Time to add some.
+
+ )
+ }
+
+ if (filteredAndSortedTasks.length === 0 && tasks.length > 0) {
+ return (
+
+ No tasks match the current filter.
+
+ )
+ }
+
+ return (
+
+ {filteredAndSortedTasks.map((task) => (
+
+ ))}
+
+ )
+}
\ No newline at end of file
diff --git a/src/stores/useTodoStore.jsx b/src/stores/useTodoStore.jsx
new file mode 100644
index 0000000..bbf8c13
--- /dev/null
+++ b/src/stores/useTodoStore.jsx
@@ -0,0 +1,38 @@
+import { create } from 'zustand'
+
+export const useTodoStore = create((set) => ({
+ tasks: [],
+ currentFilter: 'all',
+
+ //action to add new task
+ addTask: (text, dueDate = null) =>
+ set((state) => ({
+ tasks: [
+ ...state.tasks,
+ {
+ id: Date.now().toString(),
+ text,
+ completed: false,
+ createdAt: new Date().toISOString(),
+ dueDate: dueDate ? new Date(dueDate).toISOString() : null,
+ },
+ ],
+ })),
+
+//action to toggle task if completed or not
+ toggleTask: (id) =>
+ set((state) => ({
+ tasks: state.tasks.map((task) =>
+ task.id === id ? { ...task, completed: !task.completed } : task
+ ),
+ })),
+
+//action to remove task
+ removeTask: (id) =>
+ set((state) => ({
+ tasks: state.tasks.filter((task) => task.id !== id),
+ })),
+
+//action to set the current filter
+ setFilter: (filterType) => set({ currentFilter: filterType }),
+}))
\ No newline at end of file