Files
textbee/web/components/shared/relative-time.tsx
isra el 9cb1213efc feat: show relative timestamps with the exact time on hover
Device and API key cards rendered full timestamps ("Registered at: May 18,
2023, 1:42 PM"), which wrapped and dominated the cards at 375px.

New shared RelativeTime renders "7 days ago" with the exact date and time in a
tooltip. Formatting is delegated to date-fns, already a dependency and already
used by the webhook table, so no new package and no hand-rolled date parsing.
The only local rule is a "Just now" threshold under one minute.

Details:
- formatDistanceToNowStrict, not formatDistanceToNow, so it reads "3 minutes
  ago" rather than "about 3 minutes ago".
- Renders a real <time dateTime> element, keeping the machine-readable value
  in the DOM.
- The trigger is focusable, so the exact time is reachable by keyboard and not
  hover-only.
- Invalid or missing values render a caller-supplied fallback, so a key that
  has never been used still reads "Last used never" instead of "Invalid Date".

Labels shortened to match: "Registered at:" to "Registered", "Created at:" to
"Created".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 21:27:21 +03:00

72 lines
2.1 KiB
TypeScript

'use client'
import { format, formatDistanceToNowStrict, isValid } from 'date-fns'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
// Anything under this reads as "Just now" rather than "34 seconds ago".
const JUST_NOW_MS = 60 * 1000
export function toRelativeLabel(date: Date, now: Date = new Date()): string {
if (now.getTime() - date.getTime() < JUST_NOW_MS) return 'Just now'
// Strict, so it says "3 minutes ago" instead of "about 3 minutes ago".
return formatDistanceToNowStrict(date, { addSuffix: true })
}
export function toExactLabel(date: Date): string {
return format(date, "MMM d, yyyy 'at' h:mm a")
}
/**
* A timestamp shown as "7 days ago", with the exact date and time on hover.
*
* Long absolute timestamps ("May 18, 2023, 1:42 PM") wrapped and dominated the
* device and API key cards at mobile widths. Formatting is delegated to
* date-fns; the only local rule is the "Just now" threshold.
*/
export default function RelativeTime({
value,
fallback = '-',
className,
}: {
value: string | number | Date | null | undefined
fallback?: string
className?: string
}) {
if (value === null || value === undefined || value === '') {
return <span className={className}>{fallback}</span>
}
const date = value instanceof Date ? value : new Date(value)
if (!isValid(date)) {
return <span className={className}>{fallback}</span>
}
return (
<TooltipProvider delayDuration={200}>
<Tooltip>
<TooltipTrigger asChild>
{/* A real <time> keeps the machine-readable value in the DOM, and
tabIndex means keyboard users can reach the exact timestamp too. */}
<time
dateTime={date.toISOString()}
tabIndex={0}
className={cn(
'cursor-default underline decoration-dotted underline-offset-2',
className
)}
>
{toRelativeLabel(date)}
</time>
</TooltipTrigger>
<TooltipContent>{toExactLabel(date)}</TooltipContent>
</Tooltip>
</TooltipProvider>
)
}