Website: Next.js app for accounts, linking, who's online, device sign-in and the console
apps/web: Next.js 16 (App Router), Tailwind 4 and shadcn/ui, light and dark from the system. Pages render on the server and call the API with Eden Treaty (typed by @t5de/api), forwarding the visitor's cookie; forms use server actions; sign-up, sign-in and sign-out use Better Auth's React client. /api is rewritten to the API so its cookies are first-party. Who's online follows the API's event stream (Next's gzip is off: it held the stream back). Standalone output with a Dockerfile; deploys to Vercel as is. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7afada1c63
commit
64c552330b
57 files changed
+3834
No files matched your search
@@ -0,0 +1,75 @@
|
||||
"use client";
|
||||
// Times in the visitor's own time zone and language. The server doesn't know either, so it renders times in UTC and
|
||||
// the browser replaces them once the page is running.
|
||||
import { useEffect, useState, useSyncExternalStore } from "react";
|
||||
|
||||
const noSubscription = () => () => {};
|
||||
|
||||
/** False on the server and while hydrating, true after. */
|
||||
function useInBrowser(): boolean {
|
||||
return useSyncExternalStore(
|
||||
noSubscription,
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
}
|
||||
|
||||
const units: [Intl.RelativeTimeFormatUnit, number][] = [
|
||||
["year", 365 * 24 * 3600],
|
||||
["month", 30 * 24 * 3600],
|
||||
["week", 7 * 24 * 3600],
|
||||
["day", 24 * 3600],
|
||||
["hour", 3600],
|
||||
["minute", 60],
|
||||
];
|
||||
const relative = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
|
||||
function ago(iso: string, now: number): string {
|
||||
const seconds = (new Date(iso).getTime() - now) / 1000;
|
||||
for (const [unit, size] of units) {
|
||||
if (Math.abs(seconds) >= size) return relative.format(Math.round(seconds / size), unit);
|
||||
}
|
||||
return "just now";
|
||||
}
|
||||
|
||||
/** "5 minutes ago", kept up to date. */
|
||||
export function TimeAgo({ iso }: { iso: string }) {
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setNow(Date.now()), 30_000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
const local = useInBrowser();
|
||||
return (
|
||||
<time dateTime={iso} title={format(iso, { dateStyle: "medium", timeStyle: "short" }, local)}>
|
||||
{local ? ago(iso, now) : format(iso, { dateStyle: "medium" }, false)}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
|
||||
function format(iso: string, options: Intl.DateTimeFormatOptions, local: boolean): string {
|
||||
return new Date(iso).toLocaleString(local ? undefined : "en", local ? options : { ...options, timeZone: "UTC" });
|
||||
}
|
||||
|
||||
/** A date (and time). */
|
||||
export function DateTime({ iso, dateOnly = false }: { iso: string; dateOnly?: boolean }) {
|
||||
const local = useInBrowser();
|
||||
const options: Intl.DateTimeFormatOptions = dateOnly
|
||||
? { dateStyle: "medium" }
|
||||
: { dateStyle: "medium", timeStyle: "short" };
|
||||
return <time dateTime={iso}>{format(iso, options, local)}</time>;
|
||||
}
|
||||
|
||||
/** A time of day ("3:04 PM"), with the date too when it isn't today. */
|
||||
export function Since({ iso }: { iso: string }) {
|
||||
const local = useInBrowser();
|
||||
const today = local && new Date(iso).toDateString() === new Date().toDateString();
|
||||
const options: Intl.DateTimeFormatOptions = today
|
||||
? { hour: "numeric", minute: "2-digit" }
|
||||
: { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" };
|
||||
return (
|
||||
<time dateTime={iso} title={format(iso, { dateStyle: "medium", timeStyle: "short" }, local)}>
|
||||
{format(iso, options, local)}
|
||||
</time>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user