- Install sonner; <Toaster> mounted in Providers, auto-tracks theme. - Toasts replace inline result Alerts across ServerControls, PlayerManager, BackupManager, ModManager (install/remove/restore/delete/start/stop). - PlayerManager: optimistic op/deop/whitelist/ban/pardon via onMutate + rollback; UI updates instantly before RCON round-trip. - Modrinth search results now show author + "updated Xd ago" with full timestamp on hover; downloads on its own row. - New /api/mods/updates endpoint: per-installed-mod Modrinth latest-version lookup (parallel, 30min memo). Amber "Update available" badge rendered next to installed mod rows when filenames differ. - PlayerAvatar + Modrinth icons migrated to next/image (unoptimized, size hints) — fewer layout shifts. - Login page surfaces ?error= + NextAuth error codes (CredentialsSignin, SessionRequired, etc.), preserves callbackUrl, adds autocomplete hints and role="alert". Wrapped in Suspense per Next 16 requirement. - Snapshots + backups show relative "Xh ago" with exact timestamp on hover via new lib/time.ts helper. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
23 lines
939 B
TypeScript
23 lines
939 B
TypeScript
export function timeAgo(iso: string | number | Date): string {
|
|
const d = typeof iso === "string" || typeof iso === "number" ? new Date(iso) : iso;
|
|
const sec = Math.round((Date.now() - d.getTime()) / 1000);
|
|
if (!isFinite(sec) || sec < 0) return "";
|
|
if (sec < 45) return "just now";
|
|
const min = Math.round(sec / 60);
|
|
if (min < 60) return `${min}m ago`;
|
|
const hr = Math.round(min / 60);
|
|
if (hr < 24) return `${hr}h ago`;
|
|
const day = Math.round(hr / 24);
|
|
if (day < 30) return `${day}d ago`;
|
|
const mo = Math.round(day / 30);
|
|
if (mo < 12) return `${mo}mo ago`;
|
|
const yr = Math.round(mo / 12);
|
|
return `${yr}y ago`;
|
|
}
|
|
|
|
export function formatBytes(n: number): string {
|
|
if (!isFinite(n) || n <= 0) return "0 B";
|
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
const i = Math.min(Math.floor(Math.log(n) / Math.log(1024)), units.length - 1);
|
|
return `${(n / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
}
|