"use client"; import { useId, useRef, type CSSProperties } from "react"; import { useGSAP } from "@gsap/react"; import gsap from "gsap"; import { FolderClosed, Link2, MessagesSquare } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { cn } from "@/lib/utils"; gsap.registerPlugin(useGSAP); export const TASK_CARD_PRIORITIES = { moderate: { label: "Moderate priority", color: "#ff7415" }, urgent: { label: "Urgent", color: "#fa454b" }, low: { label: "Low priority", color: "#36ad71" }, onboarding: { label: "On boarding", color: "#47b3f5" }, } as const; export type TaskCardPriority = keyof typeof TASK_CARD_PRIORITIES; export interface TaskCardAssignee { name: string; src?: string; } export interface TaskCardProps { title: string; description?: string; priority?: TaskCardPriority; priorityLabel?: string; priorityColor?: string; status?: string; assignees?: TaskCardAssignee[]; maxAssignees?: number; comments?: number; links?: number; files?: number; dueDate?: string; theme?: "dark" | "light"; radius?: number; padding?: number; shadow?: number; hoverLift?: number; avatarSize?: number; bandHeight?: number; dashedBorder?: boolean; showAssignees?: boolean; showStatus?: boolean; showFooter?: boolean; surfaceColor?: string; textColor?: string; className?: string; onOpen?: () => void; } const bounded = (value: number, fallback: number, min: number, max: number) => Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback; export function TaskCard({ title, description, priority = "moderate", priorityLabel, priorityColor, status = "In Progress", assignees = [], maxAssignees = 4, comments = 8, links = 4, files = 12, dueDate, theme = "dark", radius = 20, padding = 18, shadow = 0.7, hoverLift = 3, avatarSize = 30, bandHeight = 38, dashedBorder = true, showAssignees = true, showStatus = true, showFooter = true, surfaceColor, textColor, className, onOpen, }: TaskCardProps) { const rootRef = useRef(null); const titleId = useId(); const lift = bounded(hoverLift, 3, 0, 10); useGSAP( () => { const root = rootRef.current; if (!root || !lift) return; const media = gsap.matchMedia(); media.add("(prefers-reduced-motion: no-preference)", () => { const hover = window.matchMedia("(hover: hover)"); const motion = gsap.to(root, { y: -lift, duration: 0.35, ease: "power3.out", paused: true, }); const enter = () => { if (hover.matches) motion.play(); }; const focus = () => motion.play(); const leave = () => { if (!root.contains(document.activeElement)) motion.reverse(); }; const blur = () => { if (!root.matches(":hover")) motion.reverse(); }; root.addEventListener("pointerenter", enter); root.addEventListener("pointerleave", leave); root.addEventListener("focusin", focus); root.addEventListener("focusout", blur); return () => { root.removeEventListener("pointerenter", enter); root.removeEventListener("pointerleave", leave); root.removeEventListener("focusin", focus); root.removeEventListener("focusout", blur); }; }); return () => media.revert(); }, { scope: rootRef, dependencies: [lift], revertOnUpdate: true }, ); const light = theme === "light"; const depth = bounded(shadow, 0.7, 0, 1); const curve = bounded(radius, 20, 8, 36); const inset = bounded(padding, 18, 12, 32); const size = bounded(avatarSize, 30, 20, 44); const limit = Math.floor(bounded(maxAssignees, 4, 1, 8)); const date = dueDate ? new Date(`${dueDate}T00:00:00Z`) : null; const validDate = date && Number.isFinite(date.getTime()); const dateLabel = validDate ? new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "2-digit", year: "numeric", timeZone: "UTC", }).format(date) : undefined; const base = TASK_CARD_PRIORITIES[priority] ?? TASK_CARD_PRIORITIES.moderate; const visibleAssignees = assignees.slice(0, limit); const overflow = assignees.length - visibleAssignees.length; return (
{priorityLabel ?? base.label}
{dashedBorder && (
{showFooter && ( )}
); } export default TaskCard;