Task Weaver
The TaskWeaver is an interactive and highly polished task management Next.js component. It allows users to organize their daily tasks with custom categories, priority levels, and a clean interface featuring smooth animations powered by Framer Motion and Tailwind CSS.
Task
LISTS.
Play volleyball
Create new components
Update Documentation
Dependencies
framer-motionlucide-reactCode
"use client";
import React, { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
CheckCircle2,
Circle,
MoreVertical,
Plus,
Trash2,
Edit3,
Check,
X,
} from "lucide-react";
const INITIAL_TASKS = [
{
id: 1,
title: "Play volleyball",
category: "Playing",
priority: "High",
done: false,
},
{
id: 2,
title: "Create new components",
category: "Design",
priority: "Medium",
done: false,
},
{
id: 3,
title: "Update Documentation",
category: "Docs",
priority: "Low",
done: false,
},
];
export function TaskWeaver() {
const [tasks, setTasks] = useState(INITIAL_TASKS);
const [isFormOpen, setIsFormOpen] = useState(false);
const [openMenuId, setOpenMenuId] = useState<number | null>(null);
const [formData, setFormData] = useState({
title: "",
category: "",
priority: "Medium",
});
const [editingId, setEditingId] = useState<number | null>(null);
const handleAddTask = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.title) return;
if (editingId) {
setTasks(
tasks.map((t) => (t.id === editingId ? { ...t, ...formData } : t)),
);
} else {
const newTask = { id: Date.now(), ...formData, done: false };
setTasks([newTask, ...tasks]);
}
closeForm();
};
const closeForm = () => {
setIsFormOpen(false);
setEditingId(null);
setFormData({ title: "", category: "", priority: "Medium" });
};
const openEdit = (task: any) => {
setEditingId(task.id);
setFormData({
title: task.title,
category: task.category,
priority: task.priority,
});
setIsFormOpen(true);
setOpenMenuId(null);
};
const toggleDone = (id: number) => {
setTasks(tasks.map((t) => (t.id === id ? { ...t, done: !t.done } : t)));
setOpenMenuId(null);
};
const deleteTask = (id: number) => {
setTasks(tasks.filter((t) => t.id !== id));
setOpenMenuId(null);
};
return (
<div className="p-8 rounded-[2.5rem] bg-white dark:bg-zinc-950 ring-5 ring-zinc-200 dark:ring-zinc-900 shadow-[inset_0_2px_5px_rgba(0,0,0,0.3)] max-w-md w-full relative overflow-hidden">
<div className="flex justify-between items-center mb-8 px-2">
<div>
<h3 className="text-[10px] font-black uppercase tracking-[0.3em] text-zinc-400">
Task
</h3>
<h2 className="text-2xl font-black italic opacity-80">LISTS.</h2>
</div>
<button
onClick={() => setIsFormOpen(true)}
className="p-3 rounded-2xl bg-zinc-900 dark:bg-white text-white dark:text-zinc-950 hover:scale-110 active:scale-95 transition-all shadow-lg"
>
<Plus size={18} strokeWidth={3} />
</button>
</div>
<div className="space-y-3">
<AnimatePresence mode="popLayout">
{tasks.map((task) => {
const isMenuOpen = openMenuId === task.id;
return (
<motion.div
key={task.id}
layout
style={{ zIndex: isMenuOpen ? 50 : 1 }}
className={`group relative p-4 rounded-3xl border transition-all ${
task.done
? "bg-zinc-50 dark:bg-zinc-900/20 border-transparent opacity-60 shadow-md"
: "bg-white dark:bg-zinc-900 border-zinc-100 dark:border-zinc-800 shadow-md"
}`}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<div
onClick={() => toggleDone(task.id)}
className={`transition-colors ${task.done ? "text-emerald-500" : "text-zinc-300"}`}
>
{task.done ? (
<CheckCircle2 size={20} />
) : (
<Circle size={20} />
)}
</div>
<div>
<p
className={`text-sm font-bold ${task.done ? "line-through text-zinc-400" : ""}`}
>
{task.title}
</p>
<div className="flex items-center gap-2 mt-1">
<span className="text-[8px] font-black uppercase tracking-widest text-zinc-400">
{task.category}
</span>
<span className="text-zinc-200 dark:text-zinc-800">
•
</span>
<span
className={`text-[8px] font-black uppercase tracking-widest ${task.priority === "High" ? "text-rose-500" : task.priority === "Medium" ? "text-amber-500" : "text-green-500"}`}
>
{task.priority}
</span>
</div>
</div>
</div>
<div className="relative">
<button
onClick={() => setOpenMenuId(isMenuOpen ? null : task.id)}
className="p-1 text-zinc-400 hover:text-zinc-900 dark:hover:text-white transition-all"
>
<MoreVertical size={16} />
</button>
<AnimatePresence>
{isMenuOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 10 }}
className="absolute right-0 top-8 z-[60] min-w-[140px] bg-white dark:bg-zinc-900 border border-zinc-100 dark:border-zinc-800 shadow-2xl rounded-2xl p-1.5"
>
<button
onClick={() => toggleDone(task.id)}
className="w-full flex items-center gap-2 px-3 py-2 text-[10px] font-black uppercase tracking-widest text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 rounded-xl transition-colors"
>
<Check size={12} /> {task.done ? "Undone" : "Done"}
</button>
<button
onClick={() => openEdit(task)}
className="w-full flex items-center gap-2 px-3 py-2 text-[10px] font-black uppercase tracking-widest text-zinc-500 hover:bg-zinc-50 dark:hover:bg-zinc-800 rounded-xl transition-colors"
>
<Edit3 size={12} /> Edit
</button>
<button
onClick={() => deleteTask(task.id)}
className="w-full flex items-center gap-2 px-3 py-2 text-[10px] font-black uppercase tracking-widest text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-500/10 rounded-xl transition-colors"
>
<Trash2 size={12} /> Delete
</button>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</motion.div>
);
})}
</AnimatePresence>
</div>
<AnimatePresence>
{isFormOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="absolute inset-0 z-[100] bg-white/80 dark:bg-zinc-950/80 backdrop-blur-md p-8 flex flex-col justify-center"
>
<div className="flex justify-between items-center mb-8">
<h2 className="text-xl font-black italic uppercase opacity-80">
{editingId ? "Edit Task" : "New Task"}
</h2>
<button
onClick={closeForm}
className="p-2 rounded-xl bg-zinc-100 dark:bg-zinc-800 hover:bg-rose-500 hover:text-white transition-all"
>
<X size={16} />
</button>
</div>
<form onSubmit={handleAddTask} className="space-y-4">
<div className="space-y-1">
<label className="text-[8px] font-black uppercase tracking-widest text-zinc-400 ml-2">
Title
</label>
<input
required
value={formData.title}
onChange={(e) =>
setFormData({ ...formData, title: e.target.value })
}
className="w-full p-4 rounded-2xl bg-zinc-50 dark:bg-zinc-900 border-none text-xs font-bold outline-none ring-1 ring-zinc-200 dark:ring-zinc-800 focus:ring-emerald-500 transition-all"
placeholder="What needs to be done?"
/>
</div>
<div className="">
<div className="space-y-1">
<label className="text-[8px] font-black uppercase tracking-widest text-zinc-400 ml-2">
Category
</label>
<input
value={formData.category}
onChange={(e) =>
setFormData({ ...formData, category: e.target.value })
}
className="w-full p-4 rounded-2xl bg-zinc-50 dark:bg-zinc-900 border-none text-xs font-bold outline-none ring-1 ring-zinc-200 dark:ring-zinc-800 focus:ring-emerald-500 transition-all"
placeholder="e.g. Work"
/>
</div>
<div className="space-y-1 mt-4">
<label className="text-[8px] font-black uppercase tracking-widest text-zinc-400 ml-2">
Priority
</label>
<div className="w-full p-2 transition-all flex gap-2 items-center justify-between">
{["Low", "Medium", "High"].map((level) => (
<label
key={level}
className="flex items-center gap-2 text-xs font-bold cursor-pointer select-none text-zinc-700 dark:text-zinc-300"
>
<input
type="checkbox"
checked={formData.priority === level}
onChange={(e) =>
setFormData({
...formData,
priority: e.target.checked ? level : "",
})
}
className="w-4 h-4 rounded border-zinc-300 dark:border-zinc-700 text-emerald-600 focus:ring-emerald-500"
/>
<span>{level}</span>
</label>
))}
</div>
</div>
</div>
<button
type="submit"
className="w-full py-4 rounded-2xl bg-zinc-900 dark:bg-white text-white dark:text-zinc-950 text-[10px] font-black uppercase tracking-[0.2em] shadow-xl hover:scale-[1.02] active:scale-[0.98] transition-all"
>
{editingId ? "Save Changes" : "Create Task"}
</button>
</form>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Features
Full CRUD Capabilities
Easily create, edit, and delete tasks directly within the interface.
Status Toggling
Quickly mark tasks as completed (with a strikethrough) or revert them back to pending using the action menu.
Priorities and Categories
Organize your list by adding a category tag and color-coded priority levels (High, Medium, or Low).
Fluid Animations
Utilizes AnimatePresence and layout transitions to ensure a seamless experience when opening forms, adding new tasks, or removing old ones.
Responsive & Accessible
Designed with a mobile-first, compact footprint and dark-mode-ready styling.