1/** Presentation helpers shared by the pages. */
2
3export const timeAgo = (epochMs: number): string => {
4 const s = Math.max(0, Math.floor((Date.now() - epochMs) / 1000));
5 if (s < 60) return "just now";
6 const m = Math.floor(s / 60);
7 if (m < 60) return `${m} minute${m === 1 ? "" : "s"} ago`;
8 const h = Math.floor(m / 60);
9 if (h < 24) return `${h} hour${h === 1 ? "" : "s"} ago`;
10 const d = Math.floor(h / 24);
11 if (d < 30) return `${d} day${d === 1 ? "" : "s"} ago`;
12 const mo = Math.floor(d / 30);
13 if (mo < 12) return `${mo} month${mo === 1 ? "" : "s"} ago`;
14 const y = Math.floor(mo / 12);
15 return `${y} year${y === 1 ? "" : "s"} ago`;
16};
17
18export const formatBytes = (bytes: number): string => {
19 if (bytes < 1024) return `${bytes} B`;
20 const units = ["KiB", "MiB", "GiB", "TiB"];
21 let value = bytes / 1024;
22 let unit = 0;
23 while (value >= 1024 && unit < units.length - 1) {
24 value /= 1024;
25 unit++;
26 }
27 return `${value.toFixed(value >= 100 ? 0 : 1)} ${units[unit]}`;
28};
29
30export const shortOid = (oid: string): string => oid.slice(0, 7);
31
32/** First line of a commit message. */
33export const subject = (message: string): string =>
34 message.split("\n", 1)[0] ?? "";
35
36const decoder = new TextDecoder("utf-8", { fatal: true });
37
38/** Decodes bytes as UTF-8, or `null` if the content is binary. */
39export const decodeText = (bytes: Uint8Array): string | null => {
40 // Quick binary sniff: NUL byte in the first 8 KiB.
41 const head = bytes.subarray(0, 8192);
42 for (const byte of head) if (byte === 0) return null;
43 try {
44 return decoder.decode(bytes);
45 } catch {
46 return null;
47 }
48};
49
50const IMAGE_TYPES: Record<string, string> = {
51 png: "image/png",
52 jpg: "image/jpeg",
53 jpeg: "image/jpeg",
54 gif: "image/gif",
55 svg: "image/svg+xml",
56 webp: "image/webp",
57 ico: "image/x-icon",
58};
59
60export const imageMime = (path: string): string | undefined =>
61 IMAGE_TYPES[path.split(".").pop()?.toLowerCase() ?? ""];
62
63export const isMarkdown = (path: string): boolean =>
64 /\.(md|markdown)$/i.test(path);