Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Times read as people say them, and old ones as their date: every TimeAgo across the site (283 of them) said "1176d ago" for a commit three years old, since it only knew days, hours and minutes. It now says "just now", "5m ago", "3h ago", "yesterday", "4d ago" and "3w ago", then the date, "Sep 30" this year and "Jul 24, 2023" before, from lib/time-ago.ts, and the moment in full ("Oct 13, 2026, 3:04 PM UTC") shows in the site's tooltip on hover. A deployment's status no longer wraps its time in a second hint of its own. A time that can't be read shows nothing rather than "NaN". Checked: the site's typecheck, its 893 tests (new: time-ago's wording for each span, a clock slightly ahead, an unreadable time and the full moment) and a production build.

syntaqxcommitted Parent15defafBrowse files
4 files+107−200/4 viewed
+14−14
55 import { cn } from "../../lib/cn";
66 import { linkPaths } from "../../lib/compute";
77 import { type Submission, isPending } from "../../lib/pending";
8+import { exactTime, timeAgo } from "../../lib/time-ago";
89 import { Button, type ButtonSize, type ButtonVariant, buttonVariants } from "./button";
10+import { Hint } from "./hint";
911
1012 export function Field({
1113 label,
298300 );
299301 }
300302
301−const UNITS: [string, number][] = [
302− ["d", 86_400_000],
303− ["h", 3_600_000],
304− ["m", 60_000],
305−];
306−
307−/** Compact relative time such as "5m ago". */
303+/**
304+ * Compact relative time such as "5m ago", or the date once it is weeks
305+ * old (lib/time-ago.ts), with the moment in full on hover.
306+ */
308307 export function TimeAgo({ at }: { at: string | number }) {
309308 // Timestamps are RFC 3339 strings; a few older ones are still numbers.
310− const elapsed = Date.now() - new Date(at).getTime();
311− const unit = UNITS.find(([, size]) => elapsed >= size);
312− const label = unit ? `${Math.floor(elapsed / unit[1])}${unit[0]} ago` : "just now";
309+ const time = new Date(at);
310+ if (Number.isNaN(time.getTime())) return null;
313311 return (
314− // The server and the browser render at slightly different times.
315− <time dateTime={new Date(at).toISOString()} suppressHydrationWarning>
316− {label}
317− </time>
312+ <Hint label={<span suppressHydrationWarning>{exactTime(at)}</span>}>
313+ {/* The server and the browser render at slightly different times, and zones. */}
314+ <time dateTime={time.toISOString()} suppressHydrationWarning>
315+ {timeAgo(at)}
316+ </time>
317+ </Hint>
318318 );
319319 }
320320
+37−0
1+import assert from "node:assert/strict";
2+import { test } from "node:test";
3+
4+import { exactTime, timeAgo } from "./time-ago.ts";
5+
6+const NOW = Date.parse("2026-10-13T12:00:00.000Z");
7+const before = (ms: number) => new Date(NOW - ms).toISOString();
8+const MINUTE = 60_000;
9+const HOUR = 60 * MINUTE;
10+const DAY = 24 * HOUR;
11+
12+test("a moment within the last week is said in minutes, hours or days", () => {
13+ assert.equal(timeAgo(before(10_000), NOW, "UTC"), "just now");
14+ assert.equal(timeAgo(before(-30_000), NOW, "UTC"), "just now");
15+ assert.equal(timeAgo(before(50_000), NOW, "UTC"), "1m ago");
16+ assert.equal(timeAgo(before(5 * MINUTE), NOW, "UTC"), "5m ago");
17+ assert.equal(timeAgo(before(3 * HOUR), NOW, "UTC"), "3h ago");
18+ assert.equal(timeAgo(before(30 * HOUR), NOW, "UTC"), "yesterday");
19+ assert.equal(timeAgo(before(4 * DAY), NOW, "UTC"), "4d ago");
20+});
21+
22+test("a moment weeks ago is said in weeks, and older ones by their date", () => {
23+ assert.equal(timeAgo(before(10 * DAY), NOW, "UTC"), "1w ago");
24+ assert.equal(timeAgo(before(27 * DAY), NOW, "UTC"), "3w ago");
25+ assert.equal(timeAgo("2026-09-01T09:00:00.000Z", NOW, "UTC"), "Sep 1");
26+ assert.equal(timeAgo("2023-07-24T09:00:00.000Z", NOW, "UTC"), "Jul 24, 2023");
27+ assert.equal(timeAgo(Date.parse("2023-07-24T09:00:00.000Z"), NOW, "UTC"), "Jul 24, 2023");
28+});
29+
30+test("a moment that can't be read says nothing", () => {
31+ assert.equal(timeAgo("not a date", NOW), "");
32+ assert.equal(exactTime("not a date"), "");
33+});
34+
35+test("the hint gives the moment in full", () => {
36+ assert.equal(exactTime("2026-10-13T15:04:00.000Z", "UTC"), "Oct 13, 2026, 3:04 PM UTC");
37+});
+55−0
1+/**
2+ * How long ago something happened, in a few characters, as `TimeAgo`
3+ * shows it: "just now", "5m ago", "3h ago", "yesterday", "4d ago",
4+ * "3w ago", then the date itself, "Sep 30" this year and "Sep 30, 2023"
5+ * before, since "1176d ago" says less than the date does. Only plain
6+ * functions, so it is tested on its own.
7+ */
8+
9+const MINUTE = 60_000;
10+const HOUR = 60 * MINUTE;
11+const DAY = 24 * HOUR;
12+
13+/** A moment as a number, from an RFC 3339 string or (for a few older ones) a number. */
14+function timeOf(at: string | number): number {
15+ return typeof at === "number" ? at : Date.parse(at);
16+}
17+
18+export function timeAgo(at: string | number, now: number = Date.now(), timeZone?: string): string {
19+ const time = timeOf(at);
20+ if (!Number.isFinite(time)) return "";
21+ // A moment slightly ahead is a clock that differs, not the future.
22+ const elapsed = Math.max(0, now - time);
23+ if (elapsed < 45_000) return "just now";
24+ if (elapsed < HOUR) return `${Math.max(1, Math.round(elapsed / MINUTE))}m ago`;
25+ if (elapsed < DAY) return `${Math.floor(elapsed / HOUR)}h ago`;
26+ if (elapsed < 2 * DAY) return "yesterday";
27+ if (elapsed < 7 * DAY) return `${Math.floor(elapsed / DAY)}d ago`;
28+ if (elapsed < 28 * DAY) return `${Math.floor(elapsed / (7 * DAY))}w ago`;
29+ const sameYear = yearIn(time, timeZone) === yearIn(now, timeZone);
30+ return new Date(time).toLocaleDateString("en-US", {
31+ month: "short",
32+ day: "numeric",
33+ ...(sameYear ? {} : { year: "numeric" }),
34+ timeZone,
35+ });
36+}
37+
38+/** The moment in full, for the hint over a `TimeAgo`: "Oct 13, 2026, 3:04 PM". */
39+export function exactTime(at: string | number, timeZone?: string): string {
40+ const time = timeOf(at);
41+ if (!Number.isFinite(time)) return "";
42+ return new Date(time).toLocaleString("en-US", {
43+ month: "short",
44+ day: "numeric",
45+ year: "numeric",
46+ hour: "numeric",
47+ minute: "2-digit",
48+ timeZone,
49+ timeZoneName: "short",
50+ });
51+}
52+
53+function yearIn(time: number, timeZone?: string): string {
54+ return new Date(time).toLocaleDateString("en-US", { year: "numeric", timeZone });
55+}
+1−6
99 import { TimeAgo } from "../../components/ui";
1010 import { Badge } from "../../components/ui/badge";
1111 import { Card } from "../../components/ui/card";
12−import { Hint } from "../../components/ui/hint";
1312 import { DeployLink, host } from "../../components/deploy";
1413 import { DeploymentStateBadge, DeploymentStateIcon, STATE_TONE, sourceLabel } from "../../components/deployments-panel";
1514 import { deployments } from "../../lib/services.server";
223222 <span className={`font-medium ${over && status.state === "in_progress" ? "text-muted" : STATE_TONE[status.state]}`}>{STATE_WORD[status.state]}</span>
224223 <span className="text-xs text-faint">
225224 {status.creator} ·{" "}
226− <Hint label={new Date(status.created_at).toUTCString()}>
227− <span tabIndex={0}>
228− <TimeAgo at={status.created_at} />
229− </span>
230− </Hint>
225+ <TimeAgo at={status.created_at} />
231226 </span>
232227 </p>
233228 {status.description && <p className="mt-0.5 text-sm text-muted wrap-anywhere">{status.description}</p>}