From 7c89e44254caa0f61d771fe523f7f1583d74725b Mon Sep 17 00:00:00 2001 From: Idahel Date: Wed, 21 May 2025 16:00:47 +0200 Subject: [PATCH 1/3] basic structure --- package.json | 3 ++- src/App.css | 0 src/components/FilterButtons.jsx | 4 ++++ src/components/Header.jsx | 4 ++++ src/components/TodoForm.jsx | 0 src/components/TodoItem.jsx | 0 src/components/TodoList.jsx | 4 ++++ src/stores/useTodoStore.jsx | 20 ++++++++++++++++++++ 8 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 src/App.css create mode 100644 src/components/FilterButtons.jsx create mode 100644 src/components/Header.jsx create mode 100644 src/components/TodoForm.jsx create mode 100644 src/components/TodoItem.jsx create mode 100644 src/components/TodoList.jsx create mode 100644 src/stores/useTodoStore.jsx 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..e69de29 diff --git a/src/components/FilterButtons.jsx b/src/components/FilterButtons.jsx new file mode 100644 index 0000000..8520b0c --- /dev/null +++ b/src/components/FilterButtons.jsx @@ -0,0 +1,4 @@ + +export const FilterButtons = () => { + +} \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx new file mode 100644 index 0000000..721b35e --- /dev/null +++ b/src/components/Header.jsx @@ -0,0 +1,4 @@ + +export const Header = () => { + +} \ No newline at end of file diff --git a/src/components/TodoForm.jsx b/src/components/TodoForm.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx new file mode 100644 index 0000000..e69de29 diff --git a/src/components/TodoList.jsx b/src/components/TodoList.jsx new file mode 100644 index 0000000..978c4b9 --- /dev/null +++ b/src/components/TodoList.jsx @@ -0,0 +1,4 @@ + +export const TodoList = () => { + +} \ No newline at end of file diff --git a/src/stores/useTodoStore.jsx b/src/stores/useTodoStore.jsx new file mode 100644 index 0000000..3cbd13d --- /dev/null +++ b/src/stores/useTodoStore.jsx @@ -0,0 +1,20 @@ +import { create } from 'zustand'; + +const useTodoStore = create((set) => ({ + tasks: [], + + addTask: (text) => + set((state) => ({ + tasks: [ + ...state.tasks, + { + id: Date.now(), + text, + completed: false, + createdAt: new Date(), + }, + ], + })), +})); + +export default useTodoStore; \ No newline at end of file From afb727d14c222bacdaac451c69d55505e01de10c Mon Sep 17 00:00:00 2001 From: Idahel Date: Fri, 23 May 2025 12:36:18 +0200 Subject: [PATCH 2/3] added all functionality and styling --- index.html | 5 +- src/App.css | 354 +++++++++++++++++++++++++++++++ src/App.jsx | 15 +- src/components/FilterButtons.jsx | 4 - src/components/Header.jsx | 6 +- src/components/TaskCount.jsx | 15 ++ src/components/TaskFilter.jsx | 30 +++ src/components/TodoForm.jsx | 38 ++++ src/components/TodoItem.jsx | 48 +++++ src/components/TodoList.jsx | 59 +++++- src/stores/useTodoStore.jsx | 32 ++- 11 files changed, 590 insertions(+), 16 deletions(-) delete mode 100644 src/components/FilterButtons.jsx create mode 100644 src/components/TaskCount.jsx create mode 100644 src/components/TaskFilter.jsx 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/src/App.css b/src/App.css index e69de29..840834f 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,354 @@ +*, +*::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; + padding: 10px; + } + + .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/FilterButtons.jsx b/src/components/FilterButtons.jsx deleted file mode 100644 index 8520b0c..0000000 --- a/src/components/FilterButtons.jsx +++ /dev/null @@ -1,4 +0,0 @@ - -export const FilterButtons = () => { - -} \ No newline at end of file diff --git a/src/components/Header.jsx b/src/components/Header.jsx index 721b35e..66ad8f8 100644 --- a/src/components/Header.jsx +++ b/src/components/Header.jsx @@ -1,4 +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 index e69de29..739464f 100644 --- a/src/components/TodoForm.jsx +++ 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 ( +
+ setTaskText(e.target.value)} + placeholder="Add a new task..." + aria-label="Add task text" + /> +
+ setDueDate(e.target.value)} + aria-label="Set due date" + /> + +
+
+ ) +} \ No newline at end of file diff --git a/src/components/TodoItem.jsx b/src/components/TodoItem.jsx index e69de29..636c374 100644 --- a/src/components/TodoItem.jsx +++ 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 index 978c4b9..2b5dfe0 100644 --- a/src/components/TodoList.jsx +++ b/src/components/TodoList.jsx @@ -1,4 +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 index 3cbd13d..bbf8c13 100644 --- a/src/stores/useTodoStore.jsx +++ b/src/stores/useTodoStore.jsx @@ -1,20 +1,38 @@ -import { create } from 'zustand'; +import { create } from 'zustand' -const useTodoStore = create((set) => ({ +export const useTodoStore = create((set) => ({ tasks: [], + currentFilter: 'all', - addTask: (text) => + //action to add new task + addTask: (text, dueDate = null) => set((state) => ({ tasks: [ ...state.tasks, { - id: Date.now(), + id: Date.now().toString(), text, completed: false, - createdAt: new Date(), + createdAt: new Date().toISOString(), + dueDate: dueDate ? new Date(dueDate).toISOString() : null, }, ], })), -})); -export default useTodoStore; \ No newline at end of file +//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 From 695134af0e35ca4116d00655637ee7e31fe75b2c Mon Sep 17 00:00:00 2001 From: Idahel Date: Fri, 23 May 2025 15:22:26 +0200 Subject: [PATCH 3/3] changed font size on mobile --- src/App.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/App.css b/src/App.css index 840834f..3c28a9b 100644 --- a/src/App.css +++ b/src/App.css @@ -309,7 +309,6 @@ body { .todo-form input, .todo-form button { font-size: 12px; - padding: 10px; } .todo-item {