Files
smart-home-tablet/components/cards/TasksCard.tsx
Cosmo ecf69400f6
All checks were successful
Deploy to Coolify / deploy (push) Successful in 4s
redesign: glassmorphism UI with big cards, 3-col layout, ambient orbs
2026-04-22 10:23:57 +00:00

186 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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}
/>
</>
);
}