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.

Live Preview

Task

LISTS.

Play volleyball

PlayingHigh

Create new components

DesignMedium

Update Documentation

DocsLow

Dependencies

npm installframer-motionlucide-react

Code

"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.