import { type Component, createMemo, For, Show } from "solid-js"; import { type Task, calculateUrgencyFromDate, setActiveTaskId, store, copyTask, updateTask, currentTaskContext, now, getFavoriteTag, dismissTaskUpdateIndicator, isTaskUpdatedByAnotherUser, collapseTaskUntilUpdated, expandCollapsedTask, isTaskCollapsedUntilUpdated, isTaskCollaborativelyShared, getVisibleTaskTags } from "@/store"; import { cn } from "@/lib/utils"; import { Clock, ArrowUpCircle, Copy, Users2, Star, ChevronRight } from "lucide-solid"; import { Badge } from "@/components/ui/badge"; import { useTheme } from "./ThemeProvider"; import { getThemeAdjustedColor } from "@/lib/colors"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { getStatusOptionsForTask } from "@/lib/constants"; import { getRecurrenceProgress } from "@/lib/recurrence"; import { StatusCircle } from "./StatusCircle"; export const TaskCard: Component<{ task: Task, isShaking?: boolean }> = (props) => { const { resolvedTheme } = useTheme(); const urgencyLevel = createMemo(() => calculateUrgencyFromDate(props.task.dueDate)); const urgencyColor = () => { const u = urgencyLevel(); if (u >= 8) return "text-red-500"; if (u >= 6) return "text-orange-500"; if (u >= 4) return "text-blue-500"; return "text-muted-foreground"; }; const formattedDate = () => { const date = new Date(props.task.dueDate); return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const visibleTags = createMemo(() => { return getVisibleTaskTags(props.task, currentTaskContext()); }); const isDueWithin12Hours = createMemo(() => { if (!props.task.dueDate) return false; try { const dueMs = new Date(props.task.dueDate).getTime(); const diffMs = dueMs - now(); const twelveHoursMs = 12 * 60 * 60 * 1000; return Math.abs(diffMs) <= twelveHoursMs; } catch { return false; } }); const canCollapseUntilUpdated = createMemo(() => isTaskCollaborativelyShared(props.task)); const statusOptions = createMemo(() => { const baseOptions = getStatusOptionsForTask(props.task.recurrence); if (!canCollapseUntilUpdated()) return baseOptions; return [ ...baseOptions, { value: "__collapse_until_updated__", label: "Collapse until updated" } ]; }); const recurrenceProgress = createMemo(() => { if (!props.task.completed && props.task.status !== 10) return null; return getRecurrenceProgress(props.task.recurrence, props.task.updated, now()); }); const shouldQuickloadFade = createMemo(() => store.quickloadFadeTaskIds.includes(props.task.id)); const showUpdateIndicator = createMemo(() => isTaskUpdatedByAnotherUser(props.task)); const isCollapsed = createMemo(() => isTaskCollapsedUntilUpdated(props.task)); return (
setActiveTaskId(props.task.id)} > {/* Today indicator */}
{!isCollapsed() && "Today"}
{/* Status Dropdown */}
e.stopPropagation()}> value={(props.task.status ?? 0).toString()} onChange={(val: any) => { const value = typeof val === 'object' ? val?.value : val; if (!value) return; if (value === "__collapse_until_updated__") { collapseTaskUntilUpdated(props.task.id, props.task.updated); return; } const s = parseInt(value); if (!isNaN(s)) updateTask(props.task.id, { status: s }); }} options={statusOptions()} optionValue="value" optionTextValue="label" placeholder="Status" itemComponent={(props: any) => {props.item.rawValue.label}} > { e.preventDefault(); e.stopPropagation(); updateTask(props.task.id, { status: 10 }); }} >

{props.task.title}

{props.task.content && (
{(() => { let html = props.task.content || ""; // Replace block tags with newlines first to preserve structure html = html.replace(/<\/p>/gi, '\n') .replace(/<\/div>/gi, '\n') .replace(//gi, '\n') .replace(/<\/li>/gi, '\n') .replace(/<\/h[1-6]>/gi, '\n'); const tmp = document.createElement("DIV"); tmp.innerHTML = html; const text = tmp.textContent || tmp.innerText || ""; // Replace newlines (and surrounding whitespace) with a wide gap // Using 5 non-breaking spaces worth of visual space return text.trim().replace(/\s*[\r\n]+\s*/g, ' '); })()}
)}
{/* Priority */}
P{props.task.priority}
{/* Shared Indicator - driven by task context tags */} {(() => { if (props.task.shareRefs.length === 0) return null; const tooltip = props.task.shareRefs.length === 1 ? 'Shared via 1 context' : `Shared via ${props.task.shareRefs.length} contexts`; return (
); })()} {/* Tags */} {visibleTags().length > 0 && (
{(tag) => ( { const def = store.tagDefinitions.find(d => d.name === tag); const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()); return color ? `${color}15` : undefined; })(), "border-color": (() => { const def = store.tagDefinitions.find(d => d.name === tag); const color = getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()); return color ? `${color}30` : undefined; })(), "color": (() => { const def = store.tagDefinitions.find(d => d.name === tag); return getThemeAdjustedColor(def?.color, def?.theme, resolvedTheme()) || "inherit"; })() }} >
d.name === tag)?.value ?? 5) > 5 ? "#22c55e" : (store.tagDefinitions.find(d => d.name === tag)?.value ?? 5) < 5 ? "#ef4444" : "#94a3b8" }} /> {tag} )}
)} {/* Due Date + Urgency Slide-out */}
{/* Due Date - Static Layer on Top */}
{formattedDate()}
{/* Urgency Number - Slides out from behind with fade */}
Urgency {urgencyLevel()}
} >

{props.task.title}

); };