An encrypted personal cloud built on the AT Protocol.
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}