186 lines
6.1 KiB
TypeScript
186 lines
6.1 KiB
TypeScript
"use client";
|
||
|
||
import { useState, useCallback } from "react";
|
||
import { motion, AnimatePresence } from "framer-motion";
|
||
import { CheckCircle2, Circle, Plus, ListTodo } from "lucide-react";
|
||
import { createTask, toggleTask } from "@/lib/api";
|
||
import AddTaskModal from "../AddTaskModal";
|
||
|
||
interface Task {
|
||
id: number;
|
||
title: string;
|
||
done: boolean;
|
||
priority?: number;
|
||
}
|
||
|
||
interface Props {
|
||
tasks: Task[];
|
||
onUpdate: () => void;
|
||
}
|
||
|
||
export default function TasksCard({ tasks, onUpdate }: Props) {
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [localTasks, setLocalTasks] = useState<Task[]>(tasks);
|
||
|
||
const handleToggle = useCallback(
|
||
async (task: Task) => {
|
||
setLocalTasks((prev) =>
|
||
prev.map((t) => (t.id === task.id ? { ...t, done: !t.done } : t))
|
||
);
|
||
await toggleTask(task.id, !task.done);
|
||
onUpdate();
|
||
},
|
||
[onUpdate]
|
||
);
|
||
|
||
const handleAdd = useCallback(
|
||
async (title: string) => {
|
||
const newTask = { id: Date.now(), title, done: false };
|
||
setLocalTasks((prev) => [newTask, ...prev]);
|
||
await createTask(title);
|
||
onUpdate();
|
||
},
|
||
[onUpdate]
|
||
);
|
||
|
||
const pending = localTasks.filter((t) => !t.done);
|
||
const done = localTasks.filter((t) => t.done);
|
||
|
||
return (
|
||
<>
|
||
<motion.div
|
||
className="glass-card p-6 h-full flex flex-col"
|
||
initial={{ opacity: 0, y: 20 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
transition={{ duration: 0.35, delay: 0.14 }}
|
||
whileHover={{ scale: 1.005 }}
|
||
>
|
||
{/* Header */}
|
||
<div className="flex items-center justify-between mb-4">
|
||
<div className="flex items-center gap-3">
|
||
<div
|
||
className="w-10 h-10 rounded-xl flex items-center justify-center"
|
||
style={{
|
||
background: "rgba(139,92,246,0.18)",
|
||
boxShadow: "0 0 18px rgba(139,92,246,0.3)",
|
||
}}
|
||
>
|
||
<ListTodo size={20} color="#8b5cf6" />
|
||
</div>
|
||
<div>
|
||
<div
|
||
className="text-base font-bold"
|
||
style={{ color: "var(--text-primary)" }}
|
||
>
|
||
Задачи
|
||
</div>
|
||
<div
|
||
className="text-xs"
|
||
style={{ color: "var(--text-secondary)" }}
|
||
>
|
||
{pending.length > 0
|
||
? `${pending.length} из ${localTasks.length} осталось`
|
||
: "Всё готово!"}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Add button */}
|
||
<motion.button
|
||
onClick={() => setModalOpen(true)}
|
||
className="w-10 h-10 rounded-xl flex items-center justify-center"
|
||
style={{
|
||
background: "linear-gradient(135deg, #8b5cf6, #6366f1)",
|
||
boxShadow: "0 0 18px rgba(139,92,246,0.45)",
|
||
}}
|
||
whileTap={{ scale: 0.82 }}
|
||
>
|
||
<Plus size={20} color="white" strokeWidth={2.5} />
|
||
</motion.button>
|
||
</div>
|
||
|
||
{/* Task list */}
|
||
<div className="flex-1 overflow-y-auto space-y-2 pr-0.5">
|
||
<AnimatePresence initial={false}>
|
||
{localTasks.length === 0 && (
|
||
<motion.div
|
||
className="flex flex-col items-center justify-center h-full py-8 gap-3"
|
||
initial={{ opacity: 0, scale: 0.9 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
>
|
||
<div className="text-4xl">🎉</div>
|
||
<div
|
||
className="text-sm font-medium"
|
||
style={{ color: "var(--text-secondary)" }}
|
||
>
|
||
Всё сделано на сегодня!
|
||
</div>
|
||
<motion.button
|
||
onClick={() => setModalOpen(true)}
|
||
className="mt-2 px-5 py-2.5 rounded-xl text-sm font-semibold"
|
||
style={{
|
||
background: "linear-gradient(135deg, rgba(139,92,246,0.25), rgba(99,102,241,0.2))",
|
||
border: "1px solid rgba(139,92,246,0.35)",
|
||
color: "#8b5cf6",
|
||
}}
|
||
whileTap={{ scale: 0.9 }}
|
||
>
|
||
+ Добавить задачу
|
||
</motion.button>
|
||
</motion.div>
|
||
)}
|
||
|
||
{localTasks.map((task) => (
|
||
<motion.div
|
||
key={task.id}
|
||
layout
|
||
initial={{ opacity: 0, x: -12, height: 0 }}
|
||
animate={{ opacity: 1, x: 0, height: "auto" }}
|
||
exit={{ opacity: 0, x: 12, height: 0 }}
|
||
transition={{ duration: 0.2 }}
|
||
className="flex items-center gap-3 px-4 py-3 rounded-2xl cursor-pointer"
|
||
style={{
|
||
background: task.done
|
||
? "rgba(139,92,246,0.05)"
|
||
: "rgba(255,255,255,0.04)",
|
||
border: task.done
|
||
? "1px solid rgba(139,92,246,0.15)"
|
||
: "1px solid rgba(255,255,255,0.06)",
|
||
}}
|
||
onClick={() => handleToggle(task)}
|
||
whileTap={{ scale: 0.97 }}
|
||
>
|
||
<motion.div
|
||
initial={{ scale: 0.8 }}
|
||
animate={{ scale: 1 }}
|
||
>
|
||
{task.done ? (
|
||
<CheckCircle2 size={20} color="#8b5cf6" strokeWidth={2} />
|
||
) : (
|
||
<Circle size={20} color="rgba(255,255,255,0.3)" strokeWidth={1.5} />
|
||
)}
|
||
</motion.div>
|
||
<span
|
||
className="text-sm font-medium flex-1 leading-snug"
|
||
style={{
|
||
color: task.done ? "var(--text-secondary)" : "var(--text-primary)",
|
||
textDecoration: task.done ? "line-through" : "none",
|
||
}}
|
||
>
|
||
{task.title}
|
||
</span>
|
||
</motion.div>
|
||
))}
|
||
</AnimatePresence>
|
||
</div>
|
||
</motion.div>
|
||
|
||
<AddTaskModal
|
||
open={modalOpen}
|
||
onClose={() => setModalOpen(false)}
|
||
onAdd={handleAdd}
|
||
/>
|
||
</>
|
||
);
|
||
}
|