commit dccf967fc1d2ffd604e8a3e5e32337635ad27fba
Author: tri <tri@thac.loan>
Date:   Sun Sep 28 12:10:18 2025 +0700

    style: horizontally scroll table, relative time

diff --git a/Makefile b/Makefile
index 9762b32..e3c5cc4 100644
--- a/Makefile
+++ b/Makefile
@@ -4,3 +4,6 @@ watch:
 # TODO: write a simple zig server instead
 serve:
 	python -m http.server -b localhost -d demo 8000
+
+test:
+	zig build test --watch
diff --git a/src/assets/script.js b/src/assets/script.js
new file mode 100644
index 0000000..2406b38
--- /dev/null
+++ b/src/assets/script.js
@@ -0,0 +1,48 @@
+const millisecondsPerSecond = 1000;
+const secondsPerMinute = 60;
+const minutesPerHour = 60;
+const hoursPerDay = 24;
+const daysPerWeek = 7;
+const daysPerYear = 365; // getting imprecise here, but no big deal
+const intervals = {
+  year:
+    millisecondsPerSecond *
+    secondsPerMinute *
+    minutesPerHour *
+    hoursPerDay *
+    daysPerYear,
+  week:
+    millisecondsPerSecond *
+    secondsPerMinute *
+    minutesPerHour *
+    hoursPerDay *
+    daysPerWeek,
+  day: millisecondsPerSecond * secondsPerMinute * minutesPerHour * hoursPerDay,
+  hour: millisecondsPerSecond * secondsPerMinute * minutesPerHour,
+  minute: millisecondsPerSecond * secondsPerMinute,
+  second: millisecondsPerSecond,
+};
+const relativeDateFormat = new Intl.RelativeTimeFormat("en", { style: "long" });
+
+// https://stackoverflow.com/a/78704662
+function formatRelativeTime(isoStr) {
+  const diff = new Date(isoStr) - new Date();
+  for (const interval in intervals) {
+    if (intervals[interval] <= Math.abs(diff)) {
+      return relativeDateFormat.format(
+        Math.trunc(diff / intervals[interval]),
+        interval,
+      );
+    }
+  }
+  return relativeDateFormat.format(diff / 1000, "second");
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+  document.querySelectorAll("time.relative").forEach((element) => {
+    const timeStr = element.getAttribute("datetime");
+    if (!timeStr) return;
+    const relativeTimeStr = formatRelativeTime(timeStr);
+    element.innerHTML = relativeTimeStr;
+  });
+});
diff --git a/src/assets/style.css b/src/assets/style.css
index aafc5a7..7c3d652 100644
--- a/src/assets/style.css
+++ b/src/assets/style.css
@@ -9,8 +9,9 @@
 
 table {
   text-align: left;
-  width: 100%;
   border-collapse: collapse;
+  width: 100%;
+  overflow: scroll;
 }
 /*
 tbody tr:nth-child(odd) {
@@ -29,6 +30,7 @@ th {
 }
 table {
   margin-left: -5px;
+  font-variant-numeric: tabular-nums;
 }
 
 img {
diff --git a/src/main.zig b/src/main.zig
index 3396052..00c953b 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -70,10 +70,13 @@ pub fn main() !u8 {
     }
 
     try writeHomePage(target_dir, repo_summaries.items);
-    try target_dir.writeFile(.{
-        .sub_path = "style.css",
-        .data = @embedFile("assets/style.css"),
-    });
+
+    inline for (.{ "style.css", "script.js" }) |asset_name| {
+        try target_dir.writeFile(.{
+            .sub_path = asset_name,
+            .data = @embedFile("assets/" ++ asset_name),
+        });
+    }
 
     return 0;
 }
@@ -100,51 +103,59 @@ pub fn writeHomePage(dir: fs.Dir, repos: []RepoSummary) !void {
         \\    <meta charset="utf-8" />
         \\    <title>Khoe</title>
         \\    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-        \\  <link rel="stylesheet" href="/style.css">
+        \\    <link rel="stylesheet" href="/style.css">
+        \\    <script src="/script.js"></script>
         \\  </head>
         \\  <body>
         \\    <h1>Khoe</h1>
-        \\    <p>We've got <b>{d}</b> repos in total.</p>
+        \\    <p>Listing <b>{d}</b> repos:</p>
         \\    <hr>
-        \\    <table>
-        \\      <thead>
-        \\        <tr>
-        \\          <th>Name</th>
-        \\          <th>Commits</th>
-        \\          <th>Last commit</th>
-        \\          <th>Last commit time</th>
-        \\        </tr>
-        \\      </thead>
-        \\      <tbody>
+        \\    <div style="overflow-x:auto; padding-bottom:1rem">
+        \\      <table style="white-space:nowrap">
+        \\        <thead>
+        \\          <tr>
+        \\            <th>Name</th>
+        \\            <th>Commits</th>
+        \\            <th>Last commit</th>
+        \\            <th>Last modified</th>
+        \\          </tr>
+        \\        </thead>
+        \\        <tbody>
         \\
     , .{repos.len});
 
     for (repos) |repo| {
         try writer.interface.print(
             \\<tr>
-            \\  <td style="white-space: nowrap;"><a href="/{0s}/{1s}/">{1s}</a></td>
+            \\  <td><a href="/{0s}/{1s}/">{1s}</a></td>
             \\  <td>{2d}</td>
             \\  <td>{3s}</td>
-            \\  <td style="white-space: nowrap;">{4s}</td>
+            \\  <td><time class="relative" datetime="{4s}" title="{4s}">{4s}</time></td>
             \\</tr>
             \\
         ,
             .{
-                web_prefix,
-                repo.name,
-                repo.commit_count,
-                repo.last_commit_msg,
-                repo.last_commit_time,
+                web_prefix, // 0
+                repo.name, // 1
+                repo.commit_count, // 2
+                repo.last_commit_msg, // 3
+                repo.last_commit_time, // 4
             },
         );
     }
 
     try writer.interface.writeAll(
-        \\      </tbody>
-        \\    </table>
+        \\        </tbody>
+        \\      </table>
+        \\    </div>
         \\  </body>
         \\</html>
     );
 
     try writer.interface.flush();
 }
+
+test "all" {
+    _ = @import("html.zig");
+    std.testing.refAllDeclsRecursive(@This());
+}