/** * Task card: the board's column item. Clicking opens the task detail — it * never executes anything directly (detail holds the Run button). * * Memoized: the card re-renders only when its own task record changes, so a * status/filter update on one card (or scrolling) never re-renders every * card on the board. The per-card onClick is built with a stable task reference * by the board, so the memo boundary is effective. */ import { memo } from 'react' import type { TaskRecord } from '../../core/tasks.ts' import { executionLabel, tagTone } from '../../core/tasks.ts' import { t } from '../locales.ts' import css from '../board.module.css' /** Compact relative/absolute time label. */ export function formatHostTimestamp(ms: number, timeZone?: string): string { try { return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium', timeStyle: 'medium', ...(timeZone === undefined ? {} : { timeZone }), }).format(new Date(ms)) } catch { return new Date(ms).toISOString() } } export function formatTime(ms: number, timeZone?: string): string { const date = new Date(ms) const now = Date.now() const minutes = Math.floor((now - ms) / 60000) if (minutes < 1) return t('time.justNow') if (minutes < 60) return `${minutes}m` if (minutes < 60 * 24) return `${Math.floor(minutes / 60)}h` if (timeZone !== undefined) return formatHostTimestamp(ms, timeZone) return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}` } function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord; pending: boolean; timeZone?: string; onClick: () => void }) { const latest = task.executions[task.executions.length - 1] const runs = task.executions.length const archived = task.archivedAt !== undefined const isDraggable = !archived && task.status !== 'running' && !pending return ( ) } /** Memoized card: re-renders only when the card's own task record changes. */ export const TaskCard = memo(TaskCardInner)