feat(client): polished toolbar, live online count via SSE
CI / runtime-diagnostics (push) Skipped
CI / check (push) Successful in 28s
CI / release (push) Skipped
CI / deploy (push) Successful in 1m27s

- Redesign in-game client toolbar with refined glass styling and buttons
- Live online count + emulator status streamed over SSE multicast
- Who's-online tooltip throttled to reduce repeated requests
- Fix show button so it always returns the hidden toolbar
- Use theme CSS variables instead of hardcoded colors
- Add toolbar translations across all 25 locales
This commit is contained in:
openhands committed 2026-09-02 21:14:42 +02:00
1 parent e1ecb190bc
commit 67b67798b9
32 files changed
+799 -102

No files matched your search

+33
View File
@@ -0,0 +1,33 @@
"use client";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";
import { useEventSource } from "@/lib/use-event-source";
/**
* Live online user counter. Subscribes to the SSE online-count stream so the
* displayed count updates in real-time (multicast) instead of only reflecting
* the server-rendered value at page load.
*/
export function LiveOnlineCount({
initial,
hotelName,
}: {
initial: number;
hotelName: string;
}) {
const t = useTranslations("header");
const [count, setCount] = useState(initial);
const { data: onlineStream } = useEventSource<{ count: number }>(
"/api/online/count/stream",
{ enabled: true },
);
useEffect(() => {
if (onlineStream && typeof onlineStream.count === "number") {
setCount(onlineStream.count);
}
}, [onlineStream]);
return <>{t("online", { count, hotel: hotelName })}</>;
}
+32
View File
@@ -0,0 +1,32 @@
"use client";
import { useEffect, useState } from "react";
import { AnimatedCounter } from "@/components/animated-counter";
import { useEventSource } from "@/lib/use-event-source";
/**
* Live animated online-count number. Subscribes to the SSE online-count stream
* so the stat updates in real-time (multicast) instead of only the initial SSR
* value, while keeping the entrance counter animation.
*/
export function LiveOnlineCounter({
initial,
suffix = "",
}: {
initial: number;
suffix?: string;
}) {
const [count, setCount] = useState(initial);
const { data: onlineStream } = useEventSource<{ count: number }>(
"/api/online/count/stream",
{ enabled: true },
);
useEffect(() => {
if (onlineStream && typeof onlineStream.count === "number") {
setCount(onlineStream.count);
}
}, [onlineStream]);
return <AnimatedCounter value={count} suffix={suffix} />;
}
+2 -3
View File
@@ -1,14 +1,13 @@
import { count, eq } from "drizzle-orm";
import Image from "next/image";
import { getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { LiveOnlineCount } from "@/components/live-online-count";
import { cached } from "@/lib/cache";
import { db, User } from "@/lib/db";
import { resolveHotelName } from "@/lib/hotel-name";
import { siteSettings } from "@/lib/services/site-settings";
export async function SiteHeader() {
const t = await getTranslations("header");
const [hotelName, header, logo] = await Promise.all([
resolveHotelName(),
siteSettings.get("cms_header", "/assets/images/background.png"),
@@ -140,7 +139,7 @@ export async function SiteHeader() {
className="text-xs sm:text-sm font-bold tracking-wide"
style={{ color: "rgba(255,255,255,0.9)" }}
>
{t("online", { count: online, hotel: hotelName })}
<LiveOnlineCount initial={online} hotelName={hotelName} />
</span>
</div>
</div>
+2 -3
View File
@@ -1,10 +1,10 @@
import { count, eq, inArray } from "drizzle-orm";
import Image from "next/image";
import type { Session } from "next-auth";
import { getTranslations } from "next-intl/server";
import { signOutAndRevokeTicket } from "@/actions/sessions";
import { HeaderUserIdentity } from "@/components/header-user-identity";
import Link from "@/components/link";
import { LiveOnlineCount } from "@/components/live-online-count";
import { RoomQuickEntry } from "@/components/room-quick-entry";
import { UserAvatarThumbnail } from "@/components/shared/user-avatar-thumbnail";
import { cached } from "@/lib/cache";
@@ -100,7 +100,6 @@ export async function TopHeader({ session }: { session: Session | null }) {
const textColor =
"var(--color-navbar-text-readable, var(--color-navbar-text, var(--color-text)))";
const t = await getTranslations("header");
const hotelName = await resolveHotelName();
let online: number;
@@ -173,7 +172,7 @@ export async function TopHeader({ session }: { session: Session | null }) {
className="text-[10px] sm:text-sm font-bold tracking-wide"
style={{ color: "rgba(255,255,255,0.9)" }}
>
{t("online", { count: online, hotel: hotelName })}
<LiveOnlineCount initial={online} hotelName={hotelName} />
</span>
</div>