Skip to content

Commit 7630673

Browse files
feat: Implement TagManager component for managing tags
- Added TagManager component with functionality to add, edit, and delete tags. - Integrated tag management with task context to update tasks accordingly. - Created corresponding tests for TagManager component. feat: Create GlobalTaskForm component for task creation - Developed GlobalTaskForm to handle task input and tag selection. - Implemented functionality to add new tags and select existing ones. - Added tests for GlobalTaskForm to ensure proper functionality. feat: Introduce TaskItem component for displaying individual tasks - Created TaskItem component to represent a single task with its details. - Implemented toggle and delete functionality for tasks. - Added tests for TaskItem to verify rendering and interactions. feat: Build TaskList component to display a list of tasks - Developed TaskList component to render multiple TaskItem components. - Included animations for task rendering and empty state messaging. - Added tests for TaskList to ensure correct rendering and behavior. chore: Update testing setup for Vitest - Configured Vitest for testing with React components. - Mocked framer-motion to avoid animation issues during tests. - Updated setupTests.js to include cleanup after each test. chore: Add Vitest configuration file - Created vitest.config.js for configuring Vitest testing environment.
1 parent 0bc92f6 commit 7630673

22 files changed

+3481
-1656
lines changed

package-lock.json

Lines changed: 1890 additions & 1347 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
"dependencies": {
66
"@headlessui/react": "^2.2.2",
77
"@heroicons/react": "^2.2.0",
8-
"@testing-library/jest-dom": "^5.16.5",
9-
"@testing-library/react": "^13.4.0",
10-
"@testing-library/user-event": "^14.4.3",
118
"autoprefixer": "^10.4.7",
129
"framer-motion": "^12.10.4",
1310
"postcss": "^8.4.31",
@@ -48,8 +45,13 @@
4845
]
4946
},
5047
"devDependencies": {
48+
"@testing-library/jest-dom": "^6.6.3",
49+
"@testing-library/react": "^16.3.0",
50+
"@testing-library/user-event": "^14.6.1",
5151
"@vitejs/plugin-react": "^4.1.1",
52+
"jest-environment-jsdom": "^29.7.0",
5253
"jsdom": "^22.1.0",
54+
"msw": "^2.7.6",
5355
"vite": "^4.5.3",
5456
"vitest": "^0.34.6"
5557
}

src/App.jsx

Lines changed: 57 additions & 177 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,69 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState } from 'react';
22
import { motion, AnimatePresence } from 'framer-motion';
3-
import GlobalTaskForm from './components/GlobalTaskForm';
4-
import TaskBoard from './components/TaskBoard';
53
import { PlusIcon } from '@heroicons/react/24/outline';
64

7-
function App() {
8-
const [tasks, setTasks] = useState([]);
9-
const [tags, setTags] = useState([]);
10-
const [showInput, setShowInput] = useState(false);
11-
const [stats, setStats] = useState({ total: 0, completed: 0 });
12-
13-
useEffect(() => {
14-
// Update stats whenever tasks change
15-
const completed = tasks.filter(task => task.isCompleted).length;
16-
setStats({
17-
total: tasks.length,
18-
completed,
19-
remaining: tasks.length - completed
20-
});
21-
}, [tasks]);
22-
23-
const addTask = (task) => {
24-
const id = Math.floor(Math.random() * 10000) + 1;
25-
26-
// Check if there are any new tags that need to be added to our tags list
27-
if (task.tags && task.tags.length > 0) {
28-
const newTags = task.tags.filter(tag => !tags.includes(tag));
29-
if (newTags.length > 0) {
30-
setTags([...tags, ...newTags]);
31-
}
32-
}
33-
34-
const newTask = { id, ...task };
35-
setTasks([...tasks, newTask]);
36-
setShowInput(false);
37-
};
38-
39-
const toggleTask = (id) => {
40-
setTasks(
41-
tasks.map((task) =>
42-
task.id === id ? { ...task, isCompleted: !task.isCompleted } : task
43-
)
44-
);
45-
};
46-
47-
const deleteTask = (id) => {
48-
setTasks(tasks.filter((task) => task.id !== id));
49-
};
50-
51-
const completeAllTasks = (taskIds = null) => {
52-
if (taskIds) {
53-
// Complete specific tasks (for task lists)
54-
setTasks(tasks.map(task =>
55-
taskIds.includes(task.id) ? { ...task, isCompleted: true } : task
56-
));
57-
} else {
58-
// Complete all tasks
59-
setTasks(tasks.map(task => ({ ...task, isCompleted: true })));
60-
}
61-
};
5+
import { TaskProvider } from './context/TaskContext';
6+
import { TagProvider } from './context/TagContext';
7+
import { ListProvider } from './context/ListContext';
628

63-
const deleteCompletedTasks = (taskIds = null) => {
64-
if (taskIds && taskIds.length > 0) {
65-
// Delete specific completed tasks (for task lists)
66-
setTasks(tasks.filter(task => !taskIds.includes(task.id)));
67-
} else {
68-
// Delete all completed tasks
69-
setTasks(tasks.filter(task => !task.isCompleted));
70-
}
71-
};
9+
import GlobalTaskForm from './features/tasks/components/GlobalTaskForm';
10+
import TaskBoard from './features/lists/components/TaskBoard';
7211

73-
// Handle tag management operations
74-
const handleManageTags = (operation, oldTag, newTag = null) => {
75-
switch (operation) {
76-
case 'add':
77-
// Add a new tag if it doesn't already exist
78-
if (!tags.includes(oldTag)) {
79-
setTags([...tags, oldTag]);
80-
}
81-
break;
82-
83-
case 'edit':
84-
// Update the tag in our tags list
85-
setTags(tags.map(tag => tag === oldTag ? newTag : tag));
86-
87-
// Update all tasks that contain the old tag
88-
setTasks(tasks.map(task => {
89-
if (task.tags && task.tags.includes(oldTag)) {
90-
const updatedTags = task.tags.map(tag =>
91-
tag === oldTag ? newTag : tag
92-
);
93-
return { ...task, tags: updatedTags };
94-
}
95-
return task;
96-
}));
97-
break;
98-
99-
case 'delete':
100-
// Remove the tag from our tags list
101-
setTags(tags.filter(tag => tag !== oldTag));
102-
103-
// Remove the tag from all tasks
104-
setTasks(tasks.map(task => {
105-
if (task.tags && task.tags.includes(oldTag)) {
106-
const updatedTags = task.tags.filter(tag => tag !== oldTag);
107-
return { ...task, tags: updatedTags };
108-
}
109-
return task;
110-
}));
111-
break;
112-
113-
default:
114-
break;
115-
}
116-
};
12+
function App() {
13+
const [showInput, setShowInput] = useState(false);
11714

11815
return (
119-
<div className="App min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 flex flex-col items-center py-12 px-4">
120-
<div className="w-full max-w-6xl">
121-
<motion.div
122-
className="mb-6 bg-white rounded-2xl shadow-soft p-6"
123-
initial={{ opacity: 0, y: -20 }}
124-
animate={{ opacity: 1, y: 0 }}
125-
transition={{ duration: 0.5 }}
126-
>
127-
<div className="flex justify-between items-center mb-6">
128-
<h1 className="text-3xl font-bold text-neutral-800 tracking-tight">Task Dashboard</h1>
129-
<div className="flex items-center gap-1.5 bg-primary-50 px-3 py-1.5 rounded-full">
130-
<span className="text-primary-600 text-sm font-medium">{stats.completed}/{stats.total} done</span>
131-
</div>
132-
</div>
133-
134-
<AnimatePresence>
135-
{showInput ? (
136-
<motion.div
137-
initial={{ opacity: 0, height: 0 }}
138-
animate={{ opacity: 1, height: 'auto' }}
139-
exit={{ opacity: 0, height: 0 }}
140-
className="overflow-hidden"
16+
<TaskProvider>
17+
<TagProvider>
18+
<ListProvider>
19+
<div className="App min-h-screen bg-gradient-to-br from-primary-50 to-secondary-50 flex flex-col items-center py-12 px-4" data-testid="app">
20+
<div className="w-full max-w-6xl">
21+
<motion.div
22+
className="mb-6 bg-white rounded-2xl shadow-soft p-6"
23+
initial={{ opacity: 0, y: -20 }}
24+
animate={{ opacity: 1, y: 0 }}
25+
transition={{ duration: 0.5 }}
26+
data-testid="app-header"
14127
>
142-
<GlobalTaskForm
143-
onAdd={addTask}
144-
onCancel={() => setShowInput(false)}
145-
availableTags={tags}
146-
/>
28+
<div className="flex justify-between items-center mb-6">
29+
<h1 className="text-3xl font-bold text-neutral-800 tracking-tight">Task Dashboard</h1>
30+
{/* Stats will be displayed from TaskContext */}
31+
</div>
32+
33+
<AnimatePresence>
34+
{showInput ? (
35+
<motion.div
36+
initial={{ opacity: 0, height: 0 }}
37+
animate={{ opacity: 1, height: 'auto' }}
38+
exit={{ opacity: 0, height: 0 }}
39+
className="overflow-hidden"
40+
data-testid="task-form-container"
41+
>
42+
<GlobalTaskForm onCancel={() => setShowInput(false)} />
43+
</motion.div>
44+
) : (
45+
<motion.button
46+
className="flex items-center justify-center w-full py-3 px-4 bg-primary-500 hover:bg-primary-600 text-white rounded-xl font-medium transition-colors"
47+
onClick={() => setShowInput(true)}
48+
whileTap={{ scale: 0.97 }}
49+
initial={{ opacity: 0 }}
50+
animate={{ opacity: 1 }}
51+
data-testid="show-task-form-button"
52+
>
53+
<PlusIcon className="h-5 w-5 mr-2" />
54+
Add New Task
55+
</motion.button>
56+
)}
57+
</AnimatePresence>
14758
</motion.div>
148-
) : (
149-
<motion.button
150-
className="flex items-center justify-center w-full py-3 px-4 bg-primary-500 hover:bg-primary-600 text-white rounded-xl font-medium transition-colors"
151-
onClick={() => setShowInput(true)}
152-
whileTap={{ scale: 0.97 }}
153-
initial={{ opacity: 0 }}
154-
animate={{ opacity: 1 }}
155-
>
156-
<PlusIcon className="h-5 w-5 mr-2" />
157-
Add New Task
158-
</motion.button>
159-
)}
160-
</AnimatePresence>
161-
</motion.div>
162-
163-
{/* Task board with multiple task lists */}
164-
<TaskBoard
165-
tasks={tasks}
166-
tags={tags}
167-
toggleTask={toggleTask}
168-
deleteTask={deleteTask}
169-
completeAllTasks={completeAllTasks}
170-
deleteCompletedTasks={deleteCompletedTasks}
171-
onManageTags={handleManageTags}
172-
onAddTask={addTask}
173-
/>
174-
175-
{tasks.length === 0 && !showInput && (
176-
<motion.div
177-
className="text-center py-10 bg-white rounded-2xl shadow-soft mt-6"
178-
initial={{ opacity: 0 }}
179-
animate={{ opacity: 1 }}
180-
transition={{ delay: 0.3 }}
181-
>
182-
<p className="text-neutral-500 text-lg">No tasks yet. Add one to get started!</p>
183-
</motion.div>
184-
)}
185-
</div>
186-
</div>
59+
60+
{/* The TaskBoard component now manages all task lists */}
61+
<TaskBoard />
62+
</div>
63+
</div>
64+
</ListProvider>
65+
</TagProvider>
66+
</TaskProvider>
18767
);
18868
}
18969

src/context/ListContext.jsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import React, { createContext, useState, useContext } from 'react';
2+
3+
// Create the list context
4+
const ListContext = createContext();
5+
6+
// Custom hook for using list context
7+
export const useListContext = () => useContext(ListContext);
8+
9+
// List provider component
10+
export const ListProvider = ({ children }) => {
11+
const [taskLists, setTaskLists] = useState([
12+
{ id: 'default', title: 'All Tasks', filters: [] }
13+
]);
14+
15+
const addTaskList = () => {
16+
const newList = {
17+
id: `list-${Date.now()}`,
18+
title: 'New List',
19+
filters: []
20+
};
21+
setTaskLists([...taskLists, newList]);
22+
return newList;
23+
};
24+
25+
const updateTaskList = (id, updates) => {
26+
setTaskLists(
27+
taskLists.map(list =>
28+
list.id === id ? { ...list, ...updates } : list
29+
)
30+
);
31+
};
32+
33+
const deleteTaskList = (id) => {
34+
// Don't allow deleting the default list
35+
if (id === 'default') return;
36+
setTaskLists(taskLists.filter(list => list.id !== id));
37+
};
38+
39+
// Filter tasks according to the task list's filter configuration
40+
const getFilteredTasks = (filterConfig, tasks) => {
41+
if (!filterConfig || filterConfig.length === 0) {
42+
return tasks;
43+
}
44+
45+
return tasks.filter(task => {
46+
// ALL filters must match (AND logic)
47+
return filterConfig.every(filter => {
48+
if (filter.type === 'tag') {
49+
return task.tags && task.tags.includes(filter.value);
50+
}
51+
if (filter.type === 'completed') {
52+
return task.isCompleted === filter.value;
53+
}
54+
return true;
55+
});
56+
});
57+
};
58+
59+
return (
60+
<ListContext.Provider
61+
value={{
62+
taskLists,
63+
addTaskList,
64+
updateTaskList,
65+
deleteTaskList,
66+
getFilteredTasks
67+
}}
68+
>
69+
{children}
70+
</ListContext.Provider>
71+
);
72+
};

0 commit comments

Comments
 (0)