Skip to content

Commit 0bd727d

Browse files
feat: Add GlobalTaskForm component for creating tasks with tags
feat: Introduce ListAddTask component for adding tasks to specific lists feat: Implement TagManager component for managing tags feat: Create TaskBoard component to manage multiple task lists feat: Enhance TaskItem component with improved styling and tag display feat: Update TaskList component to include animations for task items feat: Add TaskListConfig component for configuring task lists with filters style: Update global styles and add custom animations style: Extend Tailwind CSS configuration with new color and font settings
1 parent e0ec673 commit 0bd727d

File tree

13 files changed

+3873
-206
lines changed

13 files changed

+3873
-206
lines changed

package-lock.json

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

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,18 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6+
"@headlessui/react": "^2.2.2",
7+
"@heroicons/react": "^2.2.0",
68
"@testing-library/jest-dom": "^5.16.5",
79
"@testing-library/react": "^13.4.0",
810
"@testing-library/user-event": "^14.4.3",
11+
"autoprefixer": "^10.4.7",
12+
"framer-motion": "^12.10.4",
13+
"postcss": "^8.4.31",
914
"react": "^18.2.0",
1015
"react-dom": "^18.2.0",
11-
"web-vitals": "^3.1.0",
1216
"tailwindcss": "^3.1.8",
13-
"postcss": "^8.4.31",
14-
"autoprefixer": "^10.4.7"
17+
"web-vitals": "^3.1.0"
1518
},
1619
"overrides": {
1720
"@svgr/webpack": "^8.0.1",

src/App.jsx

Lines changed: 158 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1-
import React, { useState } from 'react';
2-
import AddTask from './components/AddTask';
3-
import TaskList from './components/TaskList';
1+
import React, { useState, useEffect } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import GlobalTaskForm from './components/GlobalTaskForm';
4+
import TaskBoard from './components/TaskBoard';
5+
import { PlusIcon } from '@heroicons/react/24/outline';
46

57
function App() {
68
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]);
722

823
const addTask = (task) => {
924
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+
1034
const newTask = { id, ...task };
1135
setTasks([...tasks, newTask]);
36+
setShowInput(false);
1237
};
1338

1439
const toggleTask = (id) => {
@@ -23,23 +48,141 @@ function App() {
2348
setTasks(tasks.filter((task) => task.id !== id));
2449
};
2550

26-
const completeAllTasks = () => {
27-
setTasks(tasks.map(task => ({ ...task, isCompleted: true })));
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+
};
62+
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+
}
2871
};
2972

30-
const deleteCompletedTasks = () => {
31-
setTasks(tasks.filter(task => !task.isCompleted));
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+
}
32116
};
33117

34118
return (
35-
<div className="App bg-slate-300 h-screen text-center">
36-
<header className="App-header">
37-
<h1 className="text-4xl py-5">Todo List</h1>
38-
<AddTask onAdd={addTask} />
39-
<button className="bg-green-500 bg-green-700 my-3 mx-2 text-white p-2 rounded" onClick={completeAllTasks}>Complete All</button>
40-
<button className="bg-red-500 bg-red-700 my-3 mx-2 text-white p-2 rounded" onClick={deleteCompletedTasks}>Delete Completed</button>
41-
<TaskList tasks={tasks} toggleTask={toggleTask} deleteTask={deleteTask} />
42-
</header>
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"
141+
>
142+
<GlobalTaskForm
143+
onAdd={addTask}
144+
onCancel={() => setShowInput(false)}
145+
availableTags={tags}
146+
/>
147+
</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>
43186
</div>
44187
);
45188
}

src/components/AddTask.jsx

Lines changed: 136 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,155 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
2+
import { motion } from 'framer-motion';
3+
import { CheckIcon, XMarkIcon, TagIcon, PlusIcon } from '@heroicons/react/24/outline';
24

3-
function AddTask({ onAdd }) {
5+
function AddTask({ onAdd, onCancel, availableTags = [] }) {
46
const [text, setText] = useState('');
7+
const [tags, setTags] = useState('');
8+
const [showTagSuggestions, setShowTagSuggestions] = useState(false);
9+
const inputRef = useRef(null);
10+
const tagInputRef = useRef(null);
11+
12+
useEffect(() => {
13+
// Auto-focus input when component mounts
14+
if (inputRef.current) {
15+
inputRef.current.focus();
16+
}
17+
}, []);
518

619
const onSubmit = (e) => {
720
e.preventDefault();
8-
if (!text) return;
9-
onAdd({ text, isCompleted: false });
21+
if (!text.trim()) return;
22+
23+
// Process tags - split by commas and trim each tag
24+
const tagArray = tags.trim()
25+
? tags.split(',').map(tag => tag.trim()).filter(tag => tag !== '')
26+
: [];
27+
28+
onAdd({ text, isCompleted: false, tags: tagArray });
1029
setText('');
30+
setTags('');
31+
};
32+
33+
const handleTagSelect = (tag) => {
34+
// Get current tags as an array
35+
const currentTags = tags.trim()
36+
? tags.split(',').map(tag => tag.trim()).filter(tag => tag !== '')
37+
: [];
38+
39+
// Check if tag is already in the list
40+
if (!currentTags.includes(tag)) {
41+
const newTagsString = currentTags.length > 0
42+
? `${tags.trim()}, ${tag}`
43+
: tag;
44+
45+
setTags(newTagsString);
46+
}
47+
48+
setShowTagSuggestions(false);
49+
// Focus back on the tag input
50+
if (tagInputRef.current) {
51+
tagInputRef.current.focus();
52+
}
53+
};
54+
55+
// Filter available tags that haven't been selected yet
56+
const getFilteredTags = () => {
57+
if (!tags.trim()) return availableTags;
58+
59+
const currentTags = tags.split(',').map(t => t.trim());
60+
const lastTag = currentTags[currentTags.length - 1].toLowerCase();
61+
62+
return availableTags.filter(tag =>
63+
!currentTags.includes(tag) &&
64+
tag.toLowerCase().includes(lastTag)
65+
);
1166
};
1267

1368
return (
14-
<form className="add-form flex flex-row" onSubmit={onSubmit}>
15-
<div className="form-control basis-3/4">
69+
<form className="add-form mb-6" onSubmit={onSubmit}>
70+
<div className="relative mb-2">
1671
<input
72+
ref={inputRef}
1773
type="text"
18-
placeholder="Add New Task"
74+
placeholder="What needs to be done?"
1975
value={text}
2076
onChange={(e) => setText(e.target.value)}
21-
className="input input-bordered w-full max-w-xs h-full"
77+
className="w-full py-3 px-4 pr-24 text-neutral-800 rounded-lg border border-neutral-200 focus:border-primary-400 focus:ring-2 focus:ring-primary-200 outline-none transition-all"
78+
autoComplete="off"
2279
/>
80+
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex gap-1">
81+
<motion.button
82+
type="button"
83+
onClick={onCancel}
84+
className="p-2 text-neutral-500 hover:text-neutral-700 rounded-full hover:bg-neutral-100 transition-colors"
85+
whileTap={{ scale: 0.9 }}
86+
>
87+
<XMarkIcon className="h-5 w-5" />
88+
</motion.button>
89+
<motion.button
90+
type="submit"
91+
disabled={!text.trim()}
92+
className={`p-2 rounded-full ${
93+
text.trim()
94+
? 'text-primary-600 hover:text-primary-800 hover:bg-primary-50'
95+
: 'text-neutral-300 cursor-not-allowed'
96+
} transition-colors`}
97+
whileTap={text.trim() ? { scale: 0.9 } : {}}
98+
>
99+
<CheckIcon className="h-5 w-5" />
100+
</motion.button>
101+
</div>
23102
</div>
24-
<div>
25-
<input type="submit" value="Save Task" className="bg-sky-700 hover:bg-sky-900 text-white rounded p-2" />
103+
104+
{/* Tag input field with suggestions */}
105+
<div className="relative">
106+
<div className="absolute left-3 top-1/2 -translate-y-1/2">
107+
<TagIcon className="h-4 w-4 text-neutral-500" />
108+
</div>
109+
<input
110+
ref={tagInputRef}
111+
type="text"
112+
placeholder="Add tags (comma separated, e.g. work, urgent)"
113+
value={tags}
114+
onChange={(e) => setTags(e.target.value)}
115+
onFocus={() => setShowTagSuggestions(true)}
116+
onBlur={() => setTimeout(() => setShowTagSuggestions(false), 200)}
117+
className="w-full py-2 px-4 pl-9 text-sm text-neutral-800 rounded-lg border border-neutral-200 focus:border-primary-400 focus:ring-1 focus:ring-primary-200 outline-none transition-all"
118+
autoComplete="off"
119+
/>
120+
121+
{/* Tag suggestions dropdown - improved visibility */}
122+
{showTagSuggestions && availableTags.length > 0 && (
123+
<div className="absolute z-50 mt-2 w-full bg-white rounded-md shadow-xl border border-neutral-200 max-h-56 overflow-y-auto">
124+
<div className="py-1">
125+
<div className="px-3 py-2 bg-neutral-50 border-b border-neutral-200">
126+
<span className="text-xs font-medium text-neutral-600">Available Tags</span>
127+
</div>
128+
129+
{getFilteredTags().length > 0 ? (
130+
getFilteredTags().map((tag, index) => (
131+
<div
132+
key={index}
133+
className="flex items-center px-3 py-2.5 text-sm hover:bg-primary-50 cursor-pointer transition-colors"
134+
onMouseDown={(e) => {
135+
e.preventDefault();
136+
handleTagSelect(tag);
137+
}}
138+
>
139+
<TagIcon className="h-3.5 w-3.5 mr-2 text-primary-600" />
140+
<span className="font-medium text-neutral-700">{tag}</span>
141+
</div>
142+
))
143+
) : (
144+
<div className="px-3 py-3 text-sm text-neutral-500 text-center">
145+
No matching tags
146+
</div>
147+
)}
148+
</div>
149+
</div>
150+
)}
26151
</div>
27-
</form>
152+
</form>
28153
);
29154
}
30155

0 commit comments

Comments
 (0)