An encrypted personal cloud built on the AT Protocol.
at main 118 lines 3.9 kB view raw
1// Display formatting utilities for your cabinet UI. 2 3import type { FileType } from "@/components/cabinet/types"; 4 5const MIME_TO_FILE_TYPE: ReadonlyMap<string, FileType> = new Map([ 6 ["application/pdf", "pdf"], 7 ["text/markdown", "note"], 8 ["text/plain", "document"], 9 ["text/csv", "spreadsheet"], 10 ["application/json", "code"], 11 ["application/javascript", "code"], 12 ["text/javascript", "code"], 13 ["text/typescript", "code"], 14 ["text/html", "code"], 15 ["text/css", "code"], 16 ["text/xml", "code"], 17 ["application/xml", "code"], 18 ["application/zip", "archive"], 19 ["application/gzip", "archive"], 20 ["application/x-tar", "archive"], 21 ["application/x-7z-compressed", "archive"], 22 ["application/x-rar-compressed", "archive"], 23]); 24 25const MIME_PREFIX_TO_FILE_TYPE: ReadonlyMap<string, FileType> = new Map([ 26 ["image/", "image"], 27 ["application/vnd.openxmlformats-officedocument.spreadsheetml", "spreadsheet"], 28 ["application/vnd.ms-excel", "spreadsheet"], 29 ["application/vnd.openxmlformats-officedocument.wordprocessingml", "document"], 30 ["application/msword", "document"], 31 ["application/vnd.openxmlformats-officedocument.presentationml", "document"], 32]); 33 34/** Map a MIME type to a cabinet FileType category. */ 35export function mimeTypeToFileType(mime: string): FileType { 36 const exact = MIME_TO_FILE_TYPE.get(mime); 37 if (exact) return exact; 38 39 const prefixMatch = [...MIME_PREFIX_TO_FILE_TYPE.entries()].find(([prefix]) => 40 mime.startsWith(prefix), 41 ); 42 43 return prefixMatch ? prefixMatch[1] : "document"; 44} 45 46const SIZE_UNITS = ["B", "KB", "MB", "GB", "TB"] as const; 47 48/** Format byte count as human-readable size (e.g. 1048576 → "1 MB"). */ 49export function formatFileSize(bytes: number): string { 50 if (bytes === 0) return "0 B"; 51 52 const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), SIZE_UNITS.length - 1); 53 const value = bytes / Math.pow(1024, exponent); 54 const formatted = exponent === 0 ? value.toString() : value.toFixed(value < 10 ? 1 : 0); 55 56 return `${formatted} ${SIZE_UNITS[exponent]}`; 57} 58 59const MINUTE_MS = 60_000; 60const HOUR_MS = 3_600_000; 61const DAY_MS = 86_400_000; 62 63/** Format an ISO datetime as a relative or short date string. */ 64export function formatRelativeDate(iso: string): string { 65 const then = new Date(iso); 66 const now = Date.now(); 67 const delta = now - then.getTime(); 68 69 if (delta < MINUTE_MS) return "Just now"; 70 if (delta < HOUR_MS) { 71 const minutes = Math.floor(delta / MINUTE_MS); 72 return `${minutes} min ago`; 73 } 74 if (delta < DAY_MS) { 75 const hours = Math.floor(delta / HOUR_MS); 76 return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; 77 } 78 if (delta < 2 * DAY_MS) return "Yesterday"; 79 if (delta < 7 * DAY_MS) { 80 const days = Math.floor(delta / DAY_MS); 81 return `${days} days ago`; 82 } 83 84 return then.toLocaleDateString("en-GB", { day: "numeric", month: "short" }); 85} 86 87/** Truncate a DID for display, keeping prefix and abbreviated identifier. */ 88export function truncateDid(did: string): string { 89 const lastColon = did.lastIndexOf(":"); 90 if (lastColon === -1) return did; 91 const prefix = did.slice(0, lastColon + 1); 92 const id = did.slice(lastColon + 1); 93 if (id.length <= 8) return did; 94 return `${prefix}${id.slice(0, 4)}${id.slice(-3)}`; 95} 96 97/** Format an ISO date as a short locale string (e.g. "Mar 8, 2026"). */ 98export function formatShortDate(iso: string): string { 99 try { 100 return new Date(iso).toLocaleDateString(undefined, { 101 month: "short", 102 day: "numeric", 103 year: "numeric", 104 }); 105 } catch { 106 return iso; 107 } 108} 109 110const DOCUMENT_COLLECTION = "app.opake.document"; 111const DIRECTORY_COLLECTION = "app.opake.directory"; 112 113/** Determine item kind from an AT-URI's collection segment. */ 114export function entryKindFromUri(uri: string): "file" | "folder" { 115 if (uri.includes(DIRECTORY_COLLECTION)) return "folder"; 116 if (uri.includes(DOCUMENT_COLLECTION)) return "file"; 117 return "file"; 118}