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]>
69 lines
2.7 KiB
TypeScript
69 lines
2.7 KiB
TypeScript
"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>
|
|
);
|
|
}
|