diff --git a/package.json b/package.json index 2c72eff8..fb2bc4f2 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "csv-parser": "^3.2.0", + "date-fns": "^3.6.0", "dompurify": "^3.2.7", "electron-log": "^5.4.0", "electron-updater": "^6.3.9", diff --git a/src/components/GroupedHistoryView/TaskItem.tsx b/src/components/GroupedHistoryView/TaskItem.tsx index 8ec8f7e5..dc616b99 100644 --- a/src/components/GroupedHistoryView/TaskItem.tsx +++ b/src/components/GroupedHistoryView/TaskItem.tsx @@ -12,6 +12,7 @@ import { } from "@/components/ui/popover"; import { useTranslation } from "react-i18next"; import folderIcon from "@/assets/Folder-1.svg"; +import { formatDateTime } from "@/lib/utils"; interface TaskItemProps { task: HistoryTask; @@ -72,8 +73,7 @@ export default function TaskItem({ const formatDate = (dateString?: string) => { if (!dateString) return ""; - const date = new Date(dateString); - return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + return formatDateTime(dateString, "MMM dd, yyyy HH:mm"); }; return ( diff --git a/src/components/Trigger/ExecutionLogs.tsx b/src/components/Trigger/ExecutionLogs.tsx index 068397de..29f11215 100644 --- a/src/components/Trigger/ExecutionLogs.tsx +++ b/src/components/Trigger/ExecutionLogs.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { proxyFetchTrigger, proxyFetchTriggerExecutions } from "@/service/triggerApi"; import { useActivityLogStore, ActivityType } from "@/store/activityLogStore"; import { Trigger, TriggerExecution, ExecutionStatus } from "@/types"; +import { formatTime, formatRelativeTime } from "@/lib/utils"; export interface ExecutionLogEntry { id: number; @@ -52,37 +53,6 @@ const formatDuration = (seconds?: number): string | undefined => { return `${minutes}m ${remainingSeconds.toFixed(0)}s`; }; -// Helper function to format relative time -const formatRelativeTime = (dateString?: string): string => { - if (!dateString) return "Never"; - - const date = new Date(dateString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMs / 3600000); - const diffDays = Math.floor(diffMs / 86400000); - - if (diffMins < 1) return "Just now"; - if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`; - if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`; - if (diffDays === 1) return "Yesterday"; - if (diffDays < 7) return `${diffDays} days ago`; - - return date.toLocaleDateString(); -}; - -// Helper function to format timestamp -const formatTimestamp = (dateString: string): string => { - const date = new Date(dateString); - return date.toLocaleTimeString('en-US', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - hour12: false - }); -}; - // Helper function to transform TriggerExecution to ExecutionLogEntry const transformToLogEntry = (execution: TriggerExecution): ExecutionLogEntry => { const status = mapExecutionStatus(execution.status); @@ -118,7 +88,7 @@ const transformToLogEntry = (execution: TriggerExecution): ExecutionLogEntry => return { id: execution.id, - timestamp: formatTimestamp(execution.started_at || execution.created_at || new Date().toISOString()), + timestamp: formatTime(execution.started_at || execution.created_at), status, message, duration, diff --git a/src/components/Trigger/TriggerListItem.tsx b/src/components/Trigger/TriggerListItem.tsx index df7e9a6f..e9b83852 100644 --- a/src/components/Trigger/TriggerListItem.tsx +++ b/src/components/Trigger/TriggerListItem.tsx @@ -9,6 +9,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Zap, Clock, MoreHorizontal, Edit, Copy, Trash2, Globe, MessageSquare } from "lucide-react"; import { useTranslation } from "react-i18next"; +import { formatDateTime } from "@/lib/utils"; type TriggerListItemProps = { trigger: Trigger; @@ -60,8 +61,7 @@ export const TriggerListItem: React.FC = ({ const formatLastExecution = (dateString?: string) => { if (!dateString) return t("triggers.never"); - const date = new Date(dateString); - return `${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} ${date.toLocaleDateString([], { month: 'short', day: 'numeric' })}`; + return formatDateTime(dateString, "HH:mm MMM dd"); }; return ( diff --git a/src/lib/utils.ts b/src/lib/utils.ts index bd0c391d..0a431a4b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,69 @@ import { clsx, type ClassValue } from "clsx" import { twMerge } from "tailwind-merge" +import { formatDistanceToNow, format, parseISO } from "date-fns" export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + +/** + * Date/Time Utilities + * All functions expect UTC ISO strings from API and convert to local timezone + */ + +/** + * Format UTC timestamp to local time (HH:mm:ss) + * @param utcString - ISO 8601 UTC timestamp from API + */ +export function formatTime(utcString: string | null | undefined): string { + if (!utcString) return "N/A"; + try { + const date = parseISO(utcString.endsWith('Z') ? utcString : utcString + 'Z'); + return format(date, "HH:mm:ss"); + } catch { + return "Invalid time"; + } +} + +/** + * Format UTC timestamp to local date and time + * @param utcString - ISO 8601 UTC timestamp from API + * @param formatStr - date-fns format string (default: "MMM dd, yyyy HH:mm") + */ +export function formatDateTime(utcString: string | null | undefined, formatStr: string = "MMM dd, yyyy HH:mm"): string { + if (!utcString) return "N/A"; + try { + const date = parseISO(utcString.endsWith('Z') ? utcString : utcString + 'Z'); + return format(date, formatStr); + } catch { + return "Invalid date"; + } +} + +/** + * Format UTC timestamp to local date only + * @param utcString - ISO 8601 UTC timestamp from API + */ +export function formatDate(utcString: string | null | undefined): string { + if (!utcString) return "N/A"; + try { + const date = parseISO(utcString.endsWith('Z') ? utcString : utcString + 'Z'); + return format(date, "MMM dd, yyyy"); + } catch { + return "Invalid date"; + } +} + +/** + * Format UTC timestamp as relative time (e.g., "2 hours ago") + * @param utcString - ISO 8601 UTC timestamp from API + */ +export function formatRelativeTime(utcString: string | null | undefined): string { + if (!utcString) return "N/A"; + try { + const date = parseISO(utcString.endsWith('Z') ? utcString : utcString + 'Z'); + return formatDistanceToNow(date, { addSuffix: true }); + } catch { + return "Invalid date"; + } +} diff --git a/src/pages/Project/Triggers.tsx b/src/pages/Project/Triggers.tsx index fca241a2..1460a15c 100644 --- a/src/pages/Project/Triggers.tsx +++ b/src/pages/Project/Triggers.tsx @@ -18,7 +18,7 @@ import { useTriggerStore } from "@/store/triggerStore"; import { useActivityLogStore, ActivityType } from "@/store/activityLogStore"; import { Trigger, TriggerInput, TriggerStatus } from "@/types"; import { toast } from "sonner"; -import { formatDistanceToNow } from "date-fns"; +import { formatRelativeTime } from "@/lib/utils"; import useChatStoreAdapter from "@/hooks/useChatStoreAdapter"; import { AnimatePresence, motion } from "framer-motion"; @@ -331,7 +331,7 @@ export default function Overview() { {activityLogs.slice(0, 50).map((log) => { const Icon = getActivityIcon(log.type); const notificationType = getActivityNotificationType(log.type); - const timeAgo = formatDistanceToNow(log.timestamp, { addSuffix: true }); + const timeAgo = formatRelativeTime(log.timestamp.toISOString()); return (