"use client"; import { useState, useRef, useEffect, useCallback } from "react"; import { Task, updateTask } from "@/lib/api"; import { useAuth } from "@/lib/auth"; import { useTranslation } from "@/lib/i18n"; import Link from "next/link"; import { useSwipeable } from "react-swipeable"; interface TaskCardProps { task: Task; onComplete?: (taskId: string) => void; onAssign?: (taskId: string) => void; onUpdate?: () => void; } const STATUS_CYCLE: Task["status"][] = ["pending", "in_progress", "done"]; function statusColor(status: string): string { switch (status) { case "todo": case "pending": return "#F59E0B"; // yellow case "in_progress": return "#3B82F6"; // blue case "done": return "#22C55E"; // green case "cancelled": return "#6B7280"; // gray default: return "#F59E0B"; } } function userColor(userId: string): string { const colors = [ "#3B82F6", "#8B5CF6", "#EC4899", "#F59E0B", "#10B981", "#06B6D4", "#F97316", "#6366F1", ]; let hash = 0; for (let i = 0; i < userId.length; i++) { hash = ((hash << 5) - hash) + userId.charCodeAt(i); hash |= 0; } return colors[Math.abs(hash) % colors.length]; } function isDueSoon(dateStr: string): boolean { const due = new Date(dateStr).getTime(); const now = Date.now(); return due - now <= 7 * 24 * 60 * 60 * 1000; } function isPast(dateStr: string): boolean { return new Date(dateStr).getTime() < Date.now(); } function formatDate(dateStr: string): string { return new Date(dateStr).toLocaleDateString(undefined, { month: "short", day: "numeric" }); } export default function TaskCard({ task, onComplete, onAssign, onUpdate }: TaskCardProps) { const { t } = useTranslation(); const { token } = useAuth(); const taskDone = task.status === "done"; const [swipeOffset, setSwipeOffset] = useState(0); const [swiped, setSwiped] = useState(false); const cardRef = useRef(null); // Inline editing state const [editing, setEditing] = useState(false); const [editTitle, setEditTitle] = useState(task.title); const [saving, setSaving] = useState(false); const inputRef = useRef(null); // Status cycling state const [currentStatus, setCurrentStatus] = useState(task.status); // Sync with prop changes useEffect(() => { setEditTitle(task.title); setCurrentStatus(task.status); }, [task.title, task.status]); // Focus input when entering edit mode useEffect(() => { if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.setSelectionRange(0, inputRef.current.value.length); } }, [editing]); const SWIPE_THRESHOLD = 120; const assignees = task.assigned_to || []; const visibleAssignees = assignees.slice(0, 3); const groupColor = task.group_color; const sColor = statusColor(currentStatus); const swipeHandlers = useSwipeable({ onSwiping: (e) => { if (e.dir === "Right" && !taskDone && onComplete) { setSwipeOffset(Math.min(e.deltaX, 160)); } }, onSwipedRight: (e) => { if (e.absX > SWIPE_THRESHOLD && !taskDone && onComplete) { setSwiped(true); setTimeout(() => { onComplete(task.id); }, 300); } else { setSwipeOffset(0); } }, onSwiped: () => { if (!swiped) setSwipeOffset(0); }, trackMouse: false, trackTouch: true, preventScrollOnSwipe: false, delta: 10, }); const showCompleteHint = swipeOffset > 40; // Save title edit const saveTitle = useCallback(async () => { const trimmed = editTitle.trim(); if (!trimmed || trimmed === task.title || !token) { setEditTitle(task.title); setEditing(false); return; } setSaving(true); try { await updateTask(token, task.id, { title: trimmed }); setEditing(false); if (onUpdate) onUpdate(); } catch (err) { console.error("Failed to update title:", err); setEditTitle(task.title); setEditing(false); } finally { setSaving(false); } }, [editTitle, task.title, task.id, token, onUpdate]); // Cycle status on dot click const cycleStatus = useCallback(async (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if (!token) return; const idx = STATUS_CYCLE.indexOf(currentStatus); const nextStatus = STATUS_CYCLE[(idx + 1) % STATUS_CYCLE.length]; // Optimistic update setCurrentStatus(nextStatus); try { await updateTask(token, task.id, { status: nextStatus }); if (onUpdate) onUpdate(); } catch (err) { console.error("Failed to cycle status:", err); setCurrentStatus(task.status); // revert } }, [token, task.id, task.status, currentStatus, onUpdate]); // Handle title click -> enter edit mode const handleTitleClick = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setEditing(true); }, []); // Handle keyboard in edit input const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Enter") { e.preventDefault(); saveTitle(); } if (e.key === "Escape") { setEditTitle(task.title); setEditing(false); } }, [saveTitle, task.title]); return (
{/* Swipe background */} {onComplete && !taskDone && (
SWIPE_THRESHOLD ? "bg-green-500" : "bg-green-400/80" }`} >
{t("tasks.status.done")}
)}
{/* When editing, don't wrap in Link - just show the card with input */} {editing ? (
{/* LEFT: editable input */}
setEditTitle(e.target.value)} onBlur={saveTitle} onKeyDown={handleKeyDown} disabled={saving} style={{ width: "100%", fontSize: 14, fontWeight: 500, color: "#E8E8F0", background: "#1A1A28", border: "1px solid #3B82F6", borderRadius: 6, padding: "4px 8px", outline: "none", boxShadow: "0 0 0 2px rgba(59,130,246,0.3)", opacity: saving ? 0.6 : 1, }} />
{/* RIGHT: avatars + status dot (still interactive) */}
{visibleAssignees.map((userId, i) => (
0 ? -8 : 0, border: "2px solid #13131A", background: userColor(userId), display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "white", zIndex: 3 - i, position: "relative", flexShrink: 0, }} > {userId.slice(0, 2).toUpperCase()}
))}
{ e.preventDefault(); e.stopPropagation(); if (onAssign) onAssign(task.id); }} title="Pridat uzivatele" style={{ width: 26, height: 26, borderRadius: "50%", marginLeft: visibleAssignees.length > 0 ? -8 : 0, border: "2px dashed #3A3A5A", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14, color: "#4A4A6A", position: "relative", zIndex: 0, flexShrink: 0, }} > +
{/* Status dot - clickable to cycle */}
{ (e.currentTarget as HTMLElement).style.transform = "scale(1.3)"; (e.currentTarget as HTMLElement).style.boxShadow = `0 0 10px ${sColor}`; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; (e.currentTarget as HTMLElement).style.boxShadow = `0 0 6px ${sColor}80`; }} />
) : ( /* Normal mode: Link wrapping for navigation on non-interactive areas */
{/* LEFT: title (clickable for inline edit) + optional due date */}
{ (e.currentTarget as HTMLElement).style.background = "rgba(59,130,246,0.1)"; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.background = "transparent"; }} title="Click to edit title" > {task.title}
{task.due_at && isDueSoon(task.due_at) && (
{formatDate(task.due_at)}
)}
{/* RIGHT: avatars + big status dot */}
{/* Avatars */}
{visibleAssignees.map((userId, i) => (
0 ? -8 : 0, border: "2px solid #13131A", background: userColor(userId), display: "flex", alignItems: "center", justifyContent: "center", fontSize: 10, fontWeight: 700, color: "white", zIndex: 3 - i, position: "relative", flexShrink: 0, }} > {userId.slice(0, 2).toUpperCase()}
))} {/* + add user button */}
{ e.preventDefault(); e.stopPropagation(); if (onAssign) onAssign(task.id); }} title="Pridat uzivatele" style={{ width: 26, height: 26, borderRadius: "50%", marginLeft: visibleAssignees.length > 0 ? -8 : 0, border: "2px dashed #3A3A5A", background: "transparent", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 14, color: "#4A4A6A", position: "relative", zIndex: 0, flexShrink: 0, }} > +
{/* Big colored status dot - clickable to cycle */}
{ (e.currentTarget as HTMLElement).style.transform = "scale(1.3)"; (e.currentTarget as HTMLElement).style.boxShadow = `0 0 10px ${sColor}`; }} onMouseLeave={(e) => { (e.currentTarget as HTMLElement).style.transform = "scale(1)"; (e.currentTarget as HTMLElement).style.boxShadow = `0 0 6px ${sColor}80`; }} />
)}
); }