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,68 @@
|
||||
"use client";
|
||||
// Who's online in T5DE, kept live: the page renders the list, then this follows the API's event stream.
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Initial, Outfit, Tag } from "@/components/status";
|
||||
import { Since } from "@/components/time";
|
||||
import type { OnlineEntry } from "@/lib/api";
|
||||
|
||||
export function OnlineList({ initial, moderator = false }: { initial: OnlineEntry[]; moderator?: boolean }) {
|
||||
const [online, setOnline] = useState(initial);
|
||||
const [live, setLive] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const events = new EventSource("/api/v1/online/events");
|
||||
events.addEventListener("online", (e) => {
|
||||
setOnline((JSON.parse(e.data) as { online: OnlineEntry[] }).online);
|
||||
setLive(true);
|
||||
});
|
||||
events.addEventListener("error", () => setLive(false));
|
||||
return () => events.close();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<span
|
||||
className={live ? "size-2 rounded-full bg-emerald-500" : "size-2 rounded-full bg-muted-foreground/40"}
|
||||
aria-hidden
|
||||
/>
|
||||
{live ? "Live" : "Connecting…"}
|
||||
<span className="ml-auto tabular-nums">
|
||||
{online.length} {online.length === 1 ? "account" : "accounts"}
|
||||
</span>
|
||||
</div>
|
||||
{online.length === 0 ? (
|
||||
<p className="py-6 text-sm text-muted-foreground">Nobody is showing that they're online right now.</p>
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
{online.map((o) => (
|
||||
<li key={o.imvuId} className="flex gap-4 py-4 first:pt-0 last:pb-0">
|
||||
<Initial name={o.username} />
|
||||
<div className="min-w-0 flex-1 space-y-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="font-medium">{o.username}</span>
|
||||
{o.badge && <Tag tone="strong">T5DE</Tag>}
|
||||
<Tag tone="online" dot>
|
||||
online since <Since iso={o.since} />
|
||||
</Tag>
|
||||
{o.hidden && <Tag>hidden: moderators only</Tag>}
|
||||
{moderator && o.adult && <Tag tone="adult">adult</Tag>}
|
||||
{moderator && o.personId && (
|
||||
<Link
|
||||
href={`/console/people/${o.personId}`}
|
||||
className="text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground"
|
||||
>
|
||||
person
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<Outfit items={o.outfit} />
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user