1- import { ReactElement , useMemo } from 'react' ;
1+ import { ReactElement , useEffect , useMemo , useState } from 'react' ;
22import clsx from 'clsx' ;
33import { format } from 'date-fns' ;
4- import { TimeAgo as ReactTimeAgo } from '@n1ru4l/react-time-ago' ;
4+
5+ const MINUTE = 60 ;
6+ const HOUR = MINUTE * 60 ;
7+ const DAY = HOUR * 24 ;
8+ const WEEK = DAY * 7 ;
9+ const MONTH = DAY * 30 ;
10+ const YEAR = DAY * 365 ;
11+
12+ // Terse relative label ("now" / "5m ago" / "3h ago" / "12d ago"), matching the
13+ // old @n 1ru4l/react-time-ago look, but rolling up past days into w/mo/y so old
14+ // dates stay readable (e.g. "2y ago" instead of "912d ago"). Months/years use
15+ // approximate 30/365-day buckets, which is fine at this granularity.
16+ export function formatTimeAgo ( dateObj : Date , now : number ) : string {
17+ const d = ( now - dateObj . getTime ( ) ) / 1000 ;
18+ if ( d < MINUTE * 2 ) {
19+ return 'now' ;
20+ }
21+ if ( d < HOUR ) {
22+ return `${ Math . floor ( d / MINUTE ) } m ago` ;
23+ }
24+ if ( d < DAY ) {
25+ return `${ Math . floor ( d / HOUR ) } h ago` ;
26+ }
27+ if ( d < WEEK ) {
28+ return `${ Math . floor ( d / DAY ) } d ago` ;
29+ }
30+ if ( d < MONTH ) {
31+ return `${ Math . floor ( d / WEEK ) } w ago` ;
32+ }
33+ if ( d < YEAR ) {
34+ return `${ Math . floor ( d / MONTH ) } mo ago` ;
35+ }
36+ return `${ Math . floor ( d / YEAR ) } y ago` ;
37+ }
38+
39+ // Re-render cadence for the live label. A fixed 30s tick keeps the
40+ // minute-granular output feeling live without churning (the finest unit is
41+ // minutes, so 30s always refreshes at least as often as the label can change).
42+ const REFRESH_INTERVAL_MS = 30_000 ;
543
644export const TimeAgo = ( {
745 date,
@@ -10,6 +48,8 @@ export const TimeAgo = ({
1048 date ?: string ;
1149 className ?: string ;
1250} ) : ReactElement | null => {
51+ const [ now , setNow ] = useState ( ( ) => Date . now ( ) ) ;
52+
1353 const { dateObj, formattedDate } = useMemo ( ( ) => {
1454 if ( ! date ) {
1555 return { } ;
@@ -19,21 +59,25 @@ export const TimeAgo = ({
1959 return { dateObj, formattedDate } ;
2060 } , [ date ] ) ;
2161
62+ useEffect ( ( ) => {
63+ if ( ! dateObj ) {
64+ return ;
65+ }
66+ const id = setInterval ( ( ) => setNow ( Date . now ( ) ) , REFRESH_INTERVAL_MS ) ;
67+ return ( ) => clearInterval ( id ) ;
68+ } , [ dateObj ] ) ;
69+
2270 if ( ! date || ! dateObj ) {
2371 return null ;
2472 }
2573
2674 return (
27- < ReactTimeAgo date = { dateObj } >
28- { ( { value } ) => (
29- < time
30- dateTime = { formattedDate }
31- title = { formattedDate }
32- className = { clsx ( 'cursor-default whitespace-nowrap' , className ) }
33- >
34- { value }
35- </ time >
36- ) }
37- </ ReactTimeAgo >
75+ < time
76+ dateTime = { formattedDate }
77+ title = { formattedDate }
78+ className = { clsx ( 'cursor-default whitespace-nowrap' , className ) }
79+ >
80+ { formatTimeAgo ( dateObj , now ) }
81+ </ time >
3882 ) ;
3983} ;
0 commit comments