"use client"; import { LogOut } from "lucide-react"; import Link from "next/link"; import { signOut } from "next-auth/react"; import { type ReactNode, useCallback, useEffect, useLayoutEffect, useRef, useState, } from "react"; function ToolbarBtn({ onClick, title, children, href, }: { onClick?: () => void; title: string; children: ReactNode; href?: string; }) { const cls = "w-7 h-7 sm:w-8 sm:h-8 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"; const 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-readable, var(--button-text-color, #1a1a2e))", } as const; if (href) { return ( {children} ); } return ( ); } export function ClientView({ ticket, clientUrl, hotelName, initialOnline, }: { ticket: string; clientUrl: string; hotelName: string; initialOnline: number; }) { const [_isFullscreen, setIsFullscreen] = useState(false); const [onlineCount, setOnlineCount] = useState(initialOnline); useEffect(() => { const interval = setInterval(async () => { try { const res = await fetch("/api/online/count"); if (res.ok) setOnlineCount((await res.json()).count ?? 0); } catch {} }, 15000); return () => clearInterval(interval); }, []); const toggleFullscreen = useCallback(() => { if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(() => {}); setIsFullscreen(true); } else { document.exitFullscreen().catch(() => {}); setIsFullscreen(false); } }, []); useEffect(() => { function onChange() { setIsFullscreen(!!document.fullscreenElement); } document.addEventListener("fullscreenchange", onChange); return () => document.removeEventListener("fullscreenchange", onChange); }, []); const base = clientUrl.replace(/(\?|&)sso=[^&]*/, ""); const sep = base.includes("?") ? "&" : "?"; const clientSrc = `${base}${sep}sso=${encodeURIComponent(ticket)}`; const toolbarRef = useRef(null); const dragRef = useRef({ startX: 0, startY: 0, startTop: 0, startLeft: 0 }); const posRef = useRef({ top: 8, left: 8 }); const [pos, setPos] = useState<{ top: number; left: number } | null>(null); const [dragging, setDragging] = useState(false); useLayoutEffect(() => { const params = new URLSearchParams(window.location.search); const ux = params.get("toolbarX"), uy = params.get("toolbarY"); if (ux && uy) { setPos({ top: Number(uy), left: Number(ux) }); return; } try { const sx = localStorage.getItem("client_toolbar_x"); const sy = localStorage.getItem("client_toolbar_y"); if (sx && sy) { setPos({ top: Number(sy), left: Number(sx) }); return; } } catch {} const w = toolbarRef.current?.offsetWidth ?? 220; setPos({ top: 8, left: Math.round((window.innerWidth - w) / 2) }); }, []); useEffect(() => { if (pos) posRef.current = pos; }, [pos]); const [snapTween, setSnapTween] = useState(false); const SNAP = 40; function snapPos(top: number, left: number) { const el = toolbarRef.current; if (!el) return { top, left }; const w = el.offsetWidth, h = el.offsetHeight; const vw = window.innerWidth, vh = window.innerHeight; if (top < SNAP) top = 0; else if (top + h > vh - SNAP) top = vh - h; if (left < SNAP) left = 0; else if (left + w > vw - SNAP) left = vw - w; return { top, left }; } const startDrag = useCallback( (clientX: number, clientY: number) => { if (!pos) return; setSnapTween(false); dragRef.current = { startX: clientX, startY: clientY, startTop: pos.top, startLeft: pos.left, }; setDragging(true); }, [pos], ); const handleMouseDown = useCallback( (e: React.MouseEvent) => { if ((e.target as HTMLElement).closest("button, a")) return; startDrag(e.clientX, e.clientY); }, [startDrag], ); const handleTouchStart = useCallback( (e: React.TouchEvent) => { if ((e.target as HTMLElement).closest("button, a")) return; startDrag(e.touches[0].clientX, e.touches[0].clientY); }, [startDrag], ); useEffect(() => { if (!dragging) return; const maxX = window.innerWidth - (toolbarRef.current?.offsetWidth ?? 220); const maxY = window.innerHeight - (toolbarRef.current?.offsetHeight ?? 40); function onMove(e: MouseEvent) { const dx = e.clientX - dragRef.current.startX; const dy = e.clientY - dragRef.current.startY; setPos({ top: Math.max(0, Math.min(dragRef.current.startTop + dy, maxY)), left: Math.max(0, Math.min(dragRef.current.startLeft + dx, maxX)), }); } function onTouchMove(e: TouchEvent) { const dx = e.touches[0].clientX - dragRef.current.startX; const dy = e.touches[0].clientY - dragRef.current.startY; setPos({ top: Math.max(0, Math.min(dragRef.current.startTop + dy, maxY)), left: Math.max(0, Math.min(dragRef.current.startLeft + dx, maxX)), }); } function onEnd() { setDragging(false); const snapped = snapPos(posRef.current.top, posRef.current.left); setPos(snapped); setSnapTween(true); setTimeout(() => setSnapTween(false), 180); try { localStorage.setItem("client_toolbar_x", String(snapped.left)); localStorage.setItem("client_toolbar_y", String(snapped.top)); } catch {} } window.addEventListener("mousemove", onMove); window.addEventListener("mouseup", onEnd); window.addEventListener("touchmove", onTouchMove, { passive: true }); window.addEventListener("touchend", onEnd); return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onEnd); window.removeEventListener("touchmove", onTouchMove); window.removeEventListener("touchend", onEnd); }; // biome-ignore lint/correctness/useExhaustiveDependencies: snapPos is a useCallback used intentionally here }, [dragging, snapPos]); return ( <> {pos ? ( // biome-ignore lint/a11y/noStaticElementInteractions: draggable toolbar with explicit mouse/touch handlers
Home window.location.reload()} title="Reload"> Reload Fullscreen
Online users {onlineCount}
Drag
signOut({ callbackUrl: "/" })} title="Logout" >
) : null}