feat: mobile nav, radio layout/requests, leaderboard tabs, client i18n, habbo background, navbar fixes
This commit is contained in:
1 parent
16096eff0f
commit
d6ccc250f6
20 files changed
+936
-140
No files matched your search
+74
-36
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user