Spaces:
Running
Running
| "use client"; | |
| import React, { useState, useEffect } from "react"; | |
| import { db } from "@/lib/firebase"; | |
| import { collection, onSnapshot, addDoc, updateDoc, deleteDoc, doc, query, orderBy } from "firebase/firestore"; | |
| import { motion, AnimatePresence } from "framer-motion"; | |
| import { Plus, Trash2, CheckCircle2, Circle, Clock } from "lucide-react"; | |
| interface Task { | |
| id: string; | |
| title: string; | |
| status: "todo" | "doing" | "done"; | |
| priority: "low" | "medium" | "high"; | |
| } | |
| export default function TasksPage() { | |
| const [tasks, setTasks] = useState<Task[]>([]); | |
| const [newTask, setNewTask] = useState(""); | |
| useEffect(() => { | |
| const unsub = onSnapshot(collection(db, "tasks"), (snap) => { | |
| setTasks(snap.docs.map(doc => ({ id: doc.id, ...doc.data() } as Task))); | |
| }); | |
| return () => unsub(); | |
| }, []); | |
| const addTask = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!newTask.trim()) return; | |
| await addDoc(collection(db, "tasks"), { | |
| title: newTask, | |
| status: "todo", | |
| priority: "medium", | |
| createdAt: new Date() | |
| }); | |
| setNewTask(""); | |
| }; | |
| const toggleStatus = async (task: Task) => { | |
| const nextStatus = task.status === "todo" ? "doing" : task.status === "doing" ? "done" : "todo"; | |
| await updateDoc(doc(db, "tasks", task.id), { status: nextStatus }); | |
| }; | |
| return ( | |
| <div className="p-10 max-w-6xl mx-auto"> | |
| <header className="mb-12"> | |
| <h1 className="text-5xl font-black italic tracking-tighter mb-2">TASKS / SYSTEM</h1> | |
| <p className="text-gray-500 font-medium">Gestión de flujo de trabajo corporativo</p> | |
| </header> | |
| <form onSubmit={addTask} className="mb-12 relative group"> | |
| <input | |
| type="text" | |
| placeholder="Escribe una nueva tarea y pulsa Enter..." | |
| value={newTask} | |
| onChange={(e) => setNewTask(e.target.value)} | |
| className="w-full bg-white/5 border border-white/10 rounded-3xl px-8 py-6 outline-none focus:border-blue-500/40 transition-all text-xl font-medium placeholder:text-gray-700 backdrop-blur-md" | |
| /> | |
| <button type="submit" className="absolute right-4 top-1/2 -translate-y-1/2 p-3 bg-blue-600 rounded-2xl hover:bg-blue-500 transition-all"> | |
| <Plus size={24} /> | |
| </button> | |
| </form> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-8"> | |
| <TaskColumn title="Pendientes" status="todo" tasks={tasks.filter(t => t.status === "todo")} onToggle={toggleStatus} /> | |
| <TaskColumn title="En Proceso" status="doing" tasks={tasks.filter(t => t.status === "doing")} onToggle={toggleStatus} /> | |
| <TaskColumn title="Completadas" status="done" tasks={tasks.filter(t => t.status === "done")} onToggle={toggleStatus} /> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function TaskColumn({ title, status, tasks, onToggle }: { title: string, status: string, tasks: Task[], onToggle: (t: Task) => void }) { | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="flex items-center justify-between px-4"> | |
| <h2 className="text-xs font-black uppercase tracking-[0.2em] text-gray-500">{title}</h2> | |
| <span className="text-xs font-mono bg-white/10 px-2 py-1 rounded-md">{tasks.length}</span> | |
| </div> | |
| <div className="space-y-4"> | |
| <AnimatePresence mode="popLayout"> | |
| {tasks.map((task) => ( | |
| <motion.div | |
| key={task.id} | |
| layout | |
| initial={{ scale: 0.8, opacity: 0 }} | |
| animate={{ scale: 1, opacity: 1 }} | |
| exit={{ scale: 0.8, opacity: 0 }} | |
| className="bg-white/5 border border-white/5 rounded-2xl p-5 hover:bg-white/10 hover:border-white/20 transition-all cursor-pointer group" | |
| onClick={() => onToggle(task)} | |
| > | |
| <div className="flex items-start gap-4"> | |
| <div className="mt-1"> | |
| {status === "done" ? <CheckCircle2 className="text-green-400" size={18} /> : | |
| status === "doing" ? <Clock className="text-orange-400" size={18} /> : | |
| <Circle className="text-gray-600" size={18} />} | |
| </div> | |
| <div className="flex-1"> | |
| <p className={`font-semibold text-sm ${status === "done" ? 'line-through text-gray-600' : 'text-gray-200'}`}> | |
| {task.title} | |
| </p> | |
| <div className="flex gap-2 mt-3"> | |
| <span className={`text-[8px] font-black uppercase px-2 py-0.5 rounded-full border ${ | |
| task.priority === 'high' ? 'border-red-500/40 text-red-400 bg-red-500/10' : | |
| task.priority === 'medium' ? 'border-orange-500/40 text-orange-400 bg-orange-500/10' : | |
| 'border-gray-500/40 text-gray-400 bg-gray-500/10' | |
| }`}> | |
| {task.priority || 'medium'} | |
| </span> | |
| </div> | |
| </div> | |
| <button | |
| onClick={(e) => { e.stopPropagation(); deleteDoc(doc(db, "tasks", task.id)); }} | |
| className="opacity-0 group-hover:opacity-100 p-2 hover:bg-red-500/20 text-red-400 rounded-lg transition-all" | |
| > | |
| <Trash2 size={14} /> | |
| </button> | |
| </div> | |
| </motion.div> | |
| ))} | |
| </AnimatePresence> | |
| </div> | |
| </div> | |
| ); | |
| } | |