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