Fix client loading as atomcms normal
This commit is contained in:
1 parent
bedf0ee7fa
commit
7e5813bd97
3 files changed
+150
-64
No files matched your search
+125
-53
@@ -3,7 +3,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { ContentCard, EmptyState, SkeletonCard } from "@/components/public/ui";
|
import { ContentCard, SkeletonCard } from "@/components/public/ui";
|
||||||
|
|
||||||
export default function ClientPage() {
|
export default function ClientPage() {
|
||||||
const t = useTranslations("pages.client");
|
const t = useTranslations("pages.client");
|
||||||
@@ -13,7 +13,8 @@ export default function ClientPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [errorType, setErrorType] = useState<"auth" | "generic" | null>(null);
|
const [errorType, setErrorType] = useState<"auth" | "generic" | null>(null);
|
||||||
const [hotelName, setHotelName] = useState("");
|
const [hotelName, setHotelName] = useState("");
|
||||||
const [launched, setLaunched] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
|
const [onlineCount, setOnlineCount] = useState(0);
|
||||||
const fetchedRef = useRef(false);
|
const fetchedRef = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,7 +34,7 @@ export default function ClientPage() {
|
|||||||
setTicket(data.ticket);
|
setTicket(data.ticket);
|
||||||
setHotelName(data.hotelName);
|
setHotelName(data.hotelName);
|
||||||
if (data.clientUrl) setClientUrl(data.clientUrl);
|
if (data.clientUrl) setClientUrl(data.clientUrl);
|
||||||
} catch (e) {
|
} catch {
|
||||||
setErrorType("generic");
|
setErrorType("generic");
|
||||||
setError(t("errors.generic"));
|
setError(t("errors.generic"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -43,20 +44,47 @@ export default function ClientPage() {
|
|||||||
fetchTicket();
|
fetchTicket();
|
||||||
}, [t]);
|
}, [t]);
|
||||||
|
|
||||||
const openClient = useCallback((tk: string) => {
|
useEffect(() => {
|
||||||
const sep = clientUrl.includes("?") ? "&" : "?";
|
async function fetchOnline() {
|
||||||
const fullUrl = `${clientUrl}${sep}sso=${encodeURIComponent(tk)}`;
|
try {
|
||||||
window.open(fullUrl, "_blank", "noopener,noreferrer");
|
const res = await fetch("/api/online/count");
|
||||||
}, [clientUrl]);
|
if (res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
setOnlineCount(data.count ?? 0);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetchOnline();
|
||||||
|
const interval = setInterval(fetchOnline, 15000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const toggleFullscreen = useCallback(() => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
document.documentElement.requestFullscreen();
|
||||||
|
setIsFullscreen(true);
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen();
|
||||||
|
setIsFullscreen(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ticket && !launched) {
|
function onFullscreenChange() {
|
||||||
setLaunched(true);
|
setIsFullscreen(!!document.fullscreenElement);
|
||||||
openClient(ticket);
|
|
||||||
}
|
}
|
||||||
}, [ticket, launched, openClient]);
|
document.addEventListener("fullscreenchange", onFullscreenChange);
|
||||||
|
return () => document.removeEventListener("fullscreenchange", onFullscreenChange);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const getClientSrc = useCallback(() => {
|
||||||
|
if (!ticket) return "";
|
||||||
|
const sep = clientUrl.includes("?") ? "&" : "?";
|
||||||
|
return `${clientUrl}${sep}sso=${encodeURIComponent(ticket)}`;
|
||||||
|
}, [clientUrl, ticket]);
|
||||||
|
|
||||||
// ── Loading ──
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center" style={{ backgroundColor: "var(--color-background)", padding: "2rem" }}>
|
<div className="flex min-h-screen flex-col items-center justify-center" style={{ backgroundColor: "var(--color-background)", padding: "2rem" }}>
|
||||||
@@ -65,7 +93,6 @@ export default function ClientPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Error (auth) ──
|
|
||||||
if (errorType === "auth") {
|
if (errorType === "auth") {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
|
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
|
||||||
@@ -88,7 +115,6 @@ export default function ClientPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Error (generic) ──
|
|
||||||
if (errorType === "generic" || !ticket) {
|
if (errorType === "generic" || !ticket) {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
|
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
|
||||||
@@ -99,51 +125,97 @@ export default function ClientPage() {
|
|||||||
<span className="font-bold text-lg">{hotelName || "Atom"}</span>
|
<span className="font-bold text-lg">{hotelName || "Atom"}</span>
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 flex items-center justify-center">
|
<main className="flex-1 flex items-center justify-center">
|
||||||
<ContentCard icon="⚠" title={t("title")} subtitle={error ?? t("errors.generic")}>
|
<p className="text-center" style={{ color: "var(--color-text-muted)" }}>
|
||||||
<EmptyState icon="⚠">{error ?? t("errors.generic")}</EmptyState>
|
{error ?? t("errors.generic")}
|
||||||
</ContentCard>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Success (client launched) ──
|
const clientSrc = getClientSrc();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col" style={{ backgroundColor: "var(--color-background)" }}>
|
<div className="relative w-screen h-screen overflow-hidden site-bg" style={{ backgroundColor: "var(--color-background)" }}>
|
||||||
<header
|
{/* Floating Toolbar */}
|
||||||
className="flex h-14 items-center justify-between border-b px-4"
|
<div className="fixed top-4 left-4 z-50 flex items-center gap-2">
|
||||||
style={{
|
{/* Home Button */}
|
||||||
backgroundColor: "var(--color-navbar)",
|
<Link
|
||||||
borderColor: "color-mix(in srgb, var(--color-text-muted) 15%, transparent)",
|
href="/"
|
||||||
color: "var(--color-navbar-text)",
|
className="w-10 h-10 rounded-lg border-2 cursor-pointer flex items-center justify-center shadow-lg transition-all duration-200 hover:-translate-y-0.5 hover:shadow-xl active:translate-y-0"
|
||||||
}}
|
style={{
|
||||||
>
|
borderColor: "color-mix(in srgb, var(--color-primary, #eeb425) 75%, black)",
|
||||||
<div className="flex items-center gap-4">
|
background: "linear-gradient(135deg, var(--color-primary, #eeb425) 0%, color-mix(in srgb, var(--color-primary, #eeb425) 80%, black) 100%)",
|
||||||
<span className="font-bold text-lg">{hotelName}</span>
|
color: "var(--button-text-color, #1a1a2e)",
|
||||||
<span className="text-sm" style={{ color: "var(--color-navbar-text)" }}>
|
}}
|
||||||
{t("connected")}
|
title="Home"
|
||||||
</span>
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Reload Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => window.location.reload()}
|
||||||
|
className="w-10 h-10 rounded-lg border-2 cursor-pointer flex items-center justify-center shadow-lg transition-all duration-200 hover:-translate-y-0.5 hover:shadow-xl active:translate-y-0"
|
||||||
|
style={{
|
||||||
|
borderColor: "color-mix(in srgb, var(--color-primary, #eeb425) 75%, black)",
|
||||||
|
background: "linear-gradient(135deg, var(--color-primary, #eeb425) 0%, color-mix(in srgb, var(--color-primary, #eeb425) 80%, black) 100%)",
|
||||||
|
color: "var(--button-text-color, #1a1a2e)",
|
||||||
|
}}
|
||||||
|
title="Herladen"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<polyline points="23 4 23 10 17 10" />
|
||||||
|
<polyline points="1 20 1 14 7 14" />
|
||||||
|
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Fullscreen Button */}
|
||||||
|
<button
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
className="w-10 h-10 rounded-lg border-2 cursor-pointer flex items-center justify-center shadow-lg transition-all duration-200 hover:-translate-y-0.5 hover:shadow-xl active:translate-y-0"
|
||||||
|
style={{
|
||||||
|
borderColor: "color-mix(in srgb, var(--color-primary, #eeb425) 75%, black)",
|
||||||
|
background: "linear-gradient(135deg, var(--color-primary, #eeb425) 0%, color-mix(in srgb, var(--color-primary, #eeb425) 80%, black) 100%)",
|
||||||
|
color: "var(--button-text-color, #1a1a2e)",
|
||||||
|
}}
|
||||||
|
title="Fullscreen"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Online Count Button */}
|
||||||
|
<div
|
||||||
|
className="h-10 px-3 rounded-lg border-2 flex items-center justify-center gap-1.5 shadow-lg select-none"
|
||||||
|
style={{
|
||||||
|
borderColor: "color-mix(in srgb, var(--color-primary, #eeb425) 75%, black)",
|
||||||
|
background: "linear-gradient(135deg, var(--color-primary, #eeb425) 0%, color-mix(in srgb, var(--color-primary, #eeb425) 80%, black) 100%)",
|
||||||
|
color: "var(--button-text-color, #1a1a2e)",
|
||||||
|
}}
|
||||||
|
title="Online gebruikers"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="12" cy="7" r="4" />
|
||||||
|
</svg>
|
||||||
|
<span className="text-xs font-bold">{onlineCount}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</div>
|
||||||
<main className="flex-1 flex items-center justify-center" style={{ backgroundColor: "var(--color-background)" }}>
|
|
||||||
<ContentCard>
|
{/* Game Iframe */}
|
||||||
<div style={{ textAlign: "center" }}>
|
<iframe
|
||||||
<h1 style={{ fontSize: "1.75rem", fontWeight: 700, marginBottom: "0.5rem" }}>
|
id="nitro"
|
||||||
{t("welcome", { hotel: hotelName })}
|
src={clientSrc}
|
||||||
</h1>
|
className="absolute inset-0 w-full h-full border-0 m-0 p-0 overflow-hidden"
|
||||||
<p style={{ color: "var(--color-text-muted)", marginBottom: "2rem" }}>
|
title={hotelName}
|
||||||
{t("openingClient")} {t("fallbackLink")}{" "}
|
allow="autoplay; fullscreen; gamepad"
|
||||||
<a
|
/>
|
||||||
href={`${clientUrl}${clientUrl.includes("?") ? "&" : "?"}sso=${encodeURIComponent(ticket)}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{t("fallbackClick")}
|
|
||||||
</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</ContentCard>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+24
-10
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getLocale, getMessages } from "next-intl/server";
|
import { getLocale, getMessages } from "next-intl/server";
|
||||||
import { Nunito } from "next/font/google";
|
import { Nunito } from "next/font/google";
|
||||||
|
import { headers } from "next/headers";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Navigation } from "@/components/navigation";
|
import { Navigation } from "@/components/navigation";
|
||||||
import { PwaRegister } from "@/components/pwa-register";
|
import { PwaRegister } from "@/components/pwa-register";
|
||||||
@@ -36,6 +37,12 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|||||||
// "Dusk" / dark-by-default: the site starts dark unless the visitor has picked
|
// "Dusk" / dark-by-default: the site starts dark unless the visitor has picked
|
||||||
// light. The saved choice always wins over the default.
|
// light. The saved choice always wins over the default.
|
||||||
const defaultDark = await siteSettings.getBool("default_dark", false);
|
const defaultDark = await siteSettings.getBool("default_dark", false);
|
||||||
|
|
||||||
|
const headersList = await headers();
|
||||||
|
const pathname = headersList.get("x-pathname") || "";
|
||||||
|
const isClient = pathname === "/client" || pathname.startsWith("/client/");
|
||||||
|
console.log(`[RootLayout] pathname: "${pathname}", isClient: ${isClient}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} className={`app ${nunito.variable}`}>
|
<html lang={locale} className={`app ${nunito.variable}`}>
|
||||||
<head>
|
<head>
|
||||||
@@ -52,19 +59,26 @@ export default async function RootLayout({ children }: { children: ReactNode })
|
|||||||
>
|
>
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||||
<ThemeVars />
|
<ThemeVars />
|
||||||
<TopHeader />
|
{isClient ? (
|
||||||
<SiteHeader />
|
children
|
||||||
<Navigation />
|
) : (
|
||||||
<main className="overflow-hidden site-bg">
|
<>
|
||||||
<div className="mx-auto mt-10 grid max-w-7xl grid-cols-12 gap-x-3 gap-y-8 p-6 md:mt-0">
|
<TopHeader />
|
||||||
<div className="col-span-12">{children}</div>
|
<SiteHeader />
|
||||||
</div>
|
<Navigation />
|
||||||
</main>
|
<main className="overflow-hidden site-bg">
|
||||||
<SiteFooter />
|
<div className="mx-auto mt-10 grid max-w-7xl grid-cols-12 gap-x-3 gap-y-8 p-6 md:mt-0">
|
||||||
<RadioPlayerGate />
|
<div className="col-span-12">{children}</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<SiteFooter />
|
||||||
|
<RadioPlayerGate />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<PwaRegister />
|
<PwaRegister />
|
||||||
</NextIntlClientProvider>
|
</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -234,7 +234,7 @@ export default async function HomePage() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center mb-4">
|
<div className="flex items-center mb-4">
|
||||||
<Link href="/client">
|
<Link href="/client" target="_blank" rel="noopener noreferrer">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="group rounded-full px-7 py-3 text-sm font-bold tracking-wider transition-all duration-300 hover:scale-105 active:scale-95"
|
className="group rounded-full px-7 py-3 text-sm font-bold tracking-wider transition-all duration-300 hover:scale-105 active:scale-95"
|
||||||
|
|||||||
Reference in new issue
Block a user