From 524025bfe9edcf3ef692842f6925318c6daa1c04 Mon Sep 17 00:00:00 2001 From: Admin Date: Mon, 30 Mar 2026 00:45:18 +0000 Subject: [PATCH] UI redesign: status dots with pulse + avatar circles + one-row header - TaskCard: overlapping avatars, colored status dot (pulse for active) - Header: group dropdown + status pills in single 40px row - CSS: statusPulse animation - Group interface: display_name optional field Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/tasks/components/TaskCard.tsx | 246 ++++++++++++++--------------- 1 file changed, 123 insertions(+), 123 deletions(-) diff --git a/apps/tasks/components/TaskCard.tsx b/apps/tasks/components/TaskCard.tsx index 511df28..3ab4822 100644 --- a/apps/tasks/components/TaskCard.tsx +++ b/apps/tasks/components/TaskCard.tsx @@ -9,39 +9,20 @@ import { useSwipeable } from "react-swipeable"; interface TaskCardProps { task: Task; onComplete?: (taskId: string) => void; + onAssign?: (taskId: string) => void; } -const PRIORITY_COLORS: Record = { - urgent: "#ef4444", - high: "#f97316", - medium: "#eab308", - low: "#22c55e", -}; - -const PRIORITY_ICONS: Record = { - urgent: "\u25c6", - high: "\u25b2", - medium: "\u25cf", - low: "\u25bd", -}; - -const STATUS_DOT_COLORS: Record = { - pending: "#9CA3AF", - in_progress: "#F59E0B", - done: "#22C55E", - completed: "#22C55E", - cancelled: "#EF4444", -}; - -function isDone(status: string): boolean { - return status === "done" || status === "completed"; +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 isInProgress(status: string): boolean { - return status === "in_progress"; -} - -/** Generate a consistent color from a string (user ID) */ function userColor(userId: string): string { const colors = [ "#3B82F6", "#8B5CF6", "#EC4899", "#F59E0B", @@ -55,41 +36,43 @@ function userColor(userId: string): string { return colors[Math.abs(hash) % colors.length]; } -/** Get initials from user ID (first 2 chars uppercase) */ -function userInitials(userId: string): string { - return userId.slice(0, 2).toUpperCase(); +function isDueSoon(dateStr: string): boolean { + const due = new Date(dateStr).getTime(); + const now = Date.now(); + return due - now <= 7 * 24 * 60 * 60 * 1000; } -export default function TaskCard({ task, onComplete }: TaskCardProps) { +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 }: TaskCardProps) { const { t } = useTranslation(); - const priorityColor = PRIORITY_COLORS[task.priority] || PRIORITY_COLORS.medium; - const priorityIcon = PRIORITY_ICONS[task.priority] || PRIORITY_ICONS.medium; - const statusDotColor = STATUS_DOT_COLORS[task.status] || STATUS_DOT_COLORS.pending; - const taskDone = isDone(task.status); - const taskActive = isInProgress(task.status); + const taskDone = task.status === "done"; const [swipeOffset, setSwipeOffset] = useState(0); const [swiped, setSwiped] = useState(false); const cardRef = useRef(null); const SWIPE_THRESHOLD = 120; - const MAX_AVATARS = 3; const assignees = task.assigned_to || []; - const visibleAssignees = assignees.slice(0, MAX_AVATARS); - const extraCount = assignees.length - MAX_AVATARS; + const visibleAssignees = assignees.slice(0, 3); + const groupColor = task.group_color; + const sColor = statusColor(task.status); const swipeHandlers = useSwipeable({ onSwiping: (e) => { if (e.dir === "Right" && !taskDone && onComplete) { - const offset = Math.min(e.deltaX, 160); - setSwipeOffset(offset); + setSwipeOffset(Math.min(e.deltaX, 160)); } }, onSwipedRight: (e) => { if (e.absX > SWIPE_THRESHOLD && !taskDone && onComplete) { setSwiped(true); - setTimeout(() => { - onComplete(task.id); - }, 300); + setTimeout(() => { onComplete(task.id); }, 300); } else { setSwipeOffset(0); } @@ -106,14 +89,12 @@ export default function TaskCard({ task, onComplete }: TaskCardProps) { const showCompleteHint = swipeOffset > 40; return ( -
- {/* Swipe background - green complete indicator */} +
+ {/* Swipe background */} {onComplete && !taskDone && (
SWIPE_THRESHOLD - ? "bg-green-500" - : "bg-green-400/80" + className={`absolute inset-0 flex items-center pl-4 rounded-xl transition-colors ${ + swipeOffset > SWIPE_THRESHOLD ? "bg-green-500" : "bg-green-400/80" }`} >
- +
-
+ {/* LEFT: title + optional due date */} +
+
+ {task.title} +
+ {task.due_at && isDueSoon(task.due_at) && ( +
+ {formatDate(task.due_at)} +
+ )} +
- {/* LEFT: User avatars (overlapping circles) */} -
0 ? 28 : 0 }}> - {visibleAssignees.length > 0 && ( -
- {visibleAssignees.map((userId, idx) => ( -
0 ? -8 : 0, - position: "relative", zIndex: MAX_AVATARS - idx, - }} - title={userId} - > - {userInitials(userId)} -
- ))} - {extraCount > 0 && ( -
- +{extraCount} -
- )} + {/* 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="Přidat uživatele" + 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 — big colored circle */} + {/* Big colored status dot */}
- - {/* MIDDLE: Task title — single line, truncated */} -

- {task.title} -

- - {/* RIGHT: Group icon */} - {task.group_icon && ( - {task.group_icon} - )} - - {/* RIGHT: Priority indicator */} - - {priorityIcon} -