feat: mobile nav, radio layout/requests, leaderboard tabs, client i18n, habbo background, navbar fixes

This commit is contained in:
remco committed 2026-07-01 15:59:02 +02:00
1 parent 16096eff0f
commit d6ccc250f6
20 files changed
+936 -140

No files matched your search

+74 -36
View File
@@ -1,81 +1,113 @@
"use client";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { ContentCard, EmptyState } from "@/components/public/ui";
import { useTranslations } from "next-intl";
import { ContentCard, EmptyState, SkeletonCard } from "@/components/public/ui";
export default function ClientPage() {
const t = useTranslations("pages.client");
const [ticket, setTicket] = useState<string | null>(null);
const [clientUrl, setClientUrl] = useState("/nitro-client/");
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [hotelName, setHotelName] = useState("Hubbly");
const [errorType, setErrorType] = useState<"auth" | "generic" | null>(null);
const [hotelName, setHotelName] = useState("");
const [launched, setLaunched] = useState(false);
const fetchedRef = useRef(false);
useEffect(() => {
if (fetchedRef.current) return;
fetchedRef.current = true;
async function fetchTicket() {
try {
const res = await fetch("/api/client/sso");
if (!res.ok) throw new Error("Failed to get SSO ticket");
if (res.status === 401) {
setErrorType("auth");
setError(t("errors.unauthorized"));
return;
}
if (!res.ok) throw new Error("SSO fetch failed");
const data = await res.json();
setTicket(data.ticket);
setHotelName(data.hotelName);
if (data.clientUrl) setClientUrl(data.clientUrl);
} catch (e) {
setError("Could not generate login ticket");
setErrorType("generic");
setError(t("errors.generic"));
} finally {
setLoading(false);
}
}
fetchTicket();
}, []);
}, [t]);
const openClient = useCallback((tk: string) => {
const sep = clientUrl.includes("?") ? "&" : "?";
const fullUrl = `${clientUrl}${sep}sso=${encodeURIComponent(tk)}`;
window.open(fullUrl, "_blank", "noopener,noreferrer");
}, [clientUrl]);
useEffect(() => {
if (ticket && !launched) {
setLaunched(true);
const clientUrl = `/nitro-client/?sso=${encodeURIComponent(ticket)}`;
const fullUrl = `${window.location.origin}${clientUrl}`;
window.open(fullUrl, "_blank", "noopener,noreferrer");
openClient(ticket);
}
}, [ticket, launched]);
}, [ticket, launched, openClient]);
// ── Loading ──
if (loading) {
return (
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
className="flex h-14 items-center justify-between border-b px-4"
style={{ backgroundColor: "var(--color-navbar)", borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}
>
<div className="flex items-center gap-4">
<span className="font-bold text-lg">{hotelName}</span>
<span className="text-sm text-gray-500">Loading client…</span>
</div>
</header>
<main className="flex-1 flex items-center justify-center">
<ContentCard icon="⏳" title="Loading" subtitle="Generating SSO ticket…" />
</main>
<div className="flex min-h-screen flex-col items-center justify-center" style={{ backgroundColor: "var(--color-background)", padding: "2rem" }}>
<SkeletonCard lines={2} />
</div>
);
}
if (error || !ticket) {
// ── Error (auth) ──
if (errorType === "auth") {
return (
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
className="flex h-14 items-center justify-between border-b px-4"
className="flex h-14 items-center border-b px-4"
style={{ backgroundColor: "var(--color-navbar)", borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}
>
<div className="flex items-center gap-4">
<span className="font-bold text-lg">{hotelName}</span>
</div>
<span className="font-bold text-lg">{hotelName || "Atom"}</span>
</header>
<main className="flex-1 flex items-center justify-center">
<ContentCard icon="⚠" title="Error" subtitle={error ?? "No SSO ticket"}>
<EmptyState icon="⚠">{error ?? "Could not generate SSO ticket"}</EmptyState>
<ContentCard icon="🔒" title={t("title")} subtitle={t("errors.unauthorized")}>
<div style={{ textAlign: "center", marginTop: "1rem" }}>
<Link href="/login" className="btn btn-primary">
{t("errors.loginRedirect")}
</Link>
</div>
</ContentCard>
</main>
</div>
);
}
// ── Error (generic) ──
if (errorType === "generic" || !ticket) {
return (
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
className="flex h-14 items-center border-b px-4"
style={{ backgroundColor: "var(--color-navbar)", borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)" }}
>
<span className="font-bold text-lg">{hotelName || "Atom"}</span>
</header>
<main className="flex-1 flex items-center justify-center">
<ContentCard icon="⚠" title={t("title")} subtitle={error ?? t("errors.generic")}>
<EmptyState icon="⚠">{error ?? t("errors.generic")}</EmptyState>
</ContentCard>
</main>
</div>
);
}
// ── Success (client launched) ──
return (
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
<header
@@ -88,19 +120,25 @@ export default function ClientPage() {
>
<div className="flex items-center gap-4">
<span className="font-bold text-lg">{hotelName}</span>
<span className="text-sm" style={{ color: "var(--color-navbar-text)" }}>Connected</span>
<span className="text-sm" style={{ color: "var(--color-navbar-text)" }}>
{t("connected")}
</span>
</div>
</header>
<main className="flex-1 flex items-center justify-center" style={{ backgroundColor: "var(--color-background)" }}>
<ContentCard>
<div style={{ textAlign: "center" }}>
<h1 style={{ fontSize: "1.75rem", fontWeight: 700, marginBottom: "0.5rem" }}>
Welcome to {hotelName}
{t("welcome", { hotel: hotelName })}
</h1>
<p style={{ color: "var(--color-text-muted)", marginBottom: "2rem" }}>
Opening the client in a new tab… If it doesn't open,{" "}
<a href={`/nitro-client/?sso=${encodeURIComponent(ticket)}`} target="_blank" rel="noopener noreferrer">
click here
{t("openingClient")} {t("fallbackLink")}{" "}
<a
href={`${clientUrl}${clientUrl.includes("?") ? "&" : "?"}sso=${encodeURIComponent(ticket)}`}
target="_blank"
rel="noopener noreferrer"
>
{t("fallbackClick")}
</a>.
</p>
</div>
@@ -108,4 +146,4 @@ export default function ClientPage() {
</main>
</div>
);
}
}