/meshcore/nodes: update table
Some checks failed
Test and build / Test and lint (push) Failing after 35s
Test and build / Build collector (push) Failing after 35s
Test and build / Build receiver (push) Failing after 35s

This commit is contained in:
2026-03-08 22:56:19 +01:00
parent 9053ec65a6
commit d2e710d179
7 changed files with 339 additions and 27 deletions

View 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;