/meshcore/nodes: update table
This commit is contained in:
72
ui/src/components/TimeAgo.tsx
Normal file
72
ui/src/components/TimeAgo.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import { subscribe, getSnapshot } from './TimeTicker';
|
||||
|
||||
interface TimeAgoProps {
|
||||
time?: string | null | Date;
|
||||
format?: 'short' | 'long';
|
||||
showAgo?: boolean;
|
||||
}
|
||||
|
||||
// Returns a compact label for a time difference (UTC millisecond inputs).
|
||||
function formatRelative(nowMs: number, timeMs: number, fmt: 'short' | 'long' = 'short', showAgo = true): string {
|
||||
const diffSec = Math.max(0, Math.floor((nowMs - timeMs) / 1000));
|
||||
|
||||
const units = [
|
||||
{ threshold: 60, divisor: 1, short: 's', long: 'second' },
|
||||
{ threshold: 60 * 60, divisor: 60, short: 'm', long: 'minute' },
|
||||
{ threshold: 60 * 60 * 24, divisor: 60 * 60, short: 'h', long: 'hour' },
|
||||
{ threshold: 60 * 60 * 24 * 7, divisor: 60 * 60 * 24, short: 'd', long: 'day' },
|
||||
{ threshold: 60 * 60 * 24 * 30, divisor: 60 * 60 * 24 * 7, short: 'w', long: 'week' },
|
||||
// months: fallback bucket
|
||||
{ threshold: Infinity, divisor: 60 * 60 * 24 * 30, short: 'mo', long: 'month' },
|
||||
];
|
||||
|
||||
const plural = (n: number, singular: string) => (n === 1 ? `${n} ${singular}` : `${n} ${singular}s`);
|
||||
|
||||
// find first matching unit
|
||||
for (const u of units) {
|
||||
if (diffSec < u.threshold) {
|
||||
if (fmt === 'short') {
|
||||
if (u.short === 'mo') {
|
||||
// for months, compute from days to ensure at least 1 month
|
||||
const months = Math.max(1, Math.floor(diffSec / u.divisor));
|
||||
return `${months}${u.short}${showAgo ? ' ago' : ''}`;
|
||||
}
|
||||
const value = Math.floor(diffSec / u.divisor);
|
||||
return `${value}${u.short}${showAgo ? ' ago' : ''}`;
|
||||
}
|
||||
|
||||
// long format
|
||||
if (diffSec < 1) return 'just now';
|
||||
if (u.long === 'month') {
|
||||
const months = Math.max(1, Math.floor(diffSec / u.divisor));
|
||||
return `${plural(months, u.long)}${showAgo ? ' ago' : ''}`;
|
||||
}
|
||||
const value = Math.floor(diffSec / u.divisor);
|
||||
return `${plural(value, u.long)}${showAgo ? ' ago' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
// TimeAgo subscribes to a single shared ticker (via useSyncExternalStore) so
|
||||
// many instances update efficiently without per-component timers.
|
||||
const TimeAgo: React.FC<TimeAgoProps> = ({ time, format = 'short', showAgo = true }) => {
|
||||
if (!time) return <>-</>;
|
||||
|
||||
const now = useSyncExternalStore(subscribe, getSnapshot);
|
||||
|
||||
const timeMs = typeof time === 'string'
|
||||
? Date.parse(time)
|
||||
: (time instanceof Date ? time.getTime() : NaN);
|
||||
|
||||
if (!timeMs || Number.isNaN(timeMs)) return <>-</>;
|
||||
|
||||
const label = formatRelative(now, timeMs, format, showAgo);
|
||||
|
||||
return <span title={new Date(timeMs).toLocaleString()}>{label}</span>;
|
||||
};
|
||||
|
||||
export default TimeAgo;
|
||||
Reference in New Issue
Block a user