| 1 | /** Time-ago formatter for Unix timestamps (seconds since epoch). */ |
| 2 | export function timeAgo(timestamp: number): string { |
| 3 | if (!timestamp) return ""; |
| 4 | const seconds = Math.floor(Date.now() / 1000 - timestamp); |
| 5 | if (seconds < 60) return "just now"; |
| 6 | if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; |
| 7 | if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; |
| 8 | if (seconds < 2592000) return `${Math.floor(seconds / 86400)}d ago`; |
| 9 | return new Date(timestamp * 1000).toLocaleDateString(); |
| 10 | } |
| 11 | |
| 12 | /** Time-ago formatter for ISO date strings. */ |
| 13 | export function timeAgoFromDate(dateStr: string): string { |
| 14 | const seconds = Math.floor( |
| 15 | (Date.now() - new Date(dateStr).getTime()) / 1000 |
| 16 | ); |
| 17 | if (seconds < 60) return "just now"; |
| 18 | if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; |
| 19 | if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; |
| 20 | if (seconds < 2592000) return `${Math.floor(seconds / 86400)}d ago`; |
| 21 | return new Date(dateStr).toLocaleDateString(); |
| 22 | } |
| 23 | |
| 24 | /** Human-readable file size. */ |
| 25 | export function formatSize(bytes: number): string { |
| 26 | if (bytes < 1024) return `${bytes} B`; |
| 27 | if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; |
| 28 | return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; |
| 29 | } |
| 30 | |
| 31 | /** Encode brackets in a URL path so Next.js Link doesn't treat them as dynamic params. */ |
| 32 | export function encodePath(p: string): string { |
| 33 | return p.split("/").map(s => s.replace(/\[/g, "%5B").replace(/\]/g, "%5D")).join("/"); |
| 34 | } |
| 35 | |
| 36 | /** Grove API URL for server components (SSR fetches). */ |
| 37 | export const groveApiUrl = |
| 38 | process.env.GROVE_API_URL ?? "http://localhost:4000"; |
| 39 | |