import React, { useEffect, useMemo, useRef, useState } from 'react'; import { Box, Center, Spinner, useColorModeValue } from '@chakra-ui/react'; import { useQuery } from '@tanstack/react-query'; import { getPublicScoreboard, getQr, listSponsorsPublic } from '@/services/scoreboard'; const css = ` html, body { margin: 0; padding: 0; background: transparent; height: 100%; overflow: hidden; } .bar { position: fixed; left: 0; right: 0; bottom: 0; height: 80px; background: #000000; display: flex; align-items: center; padding: 0; box-sizing: border-box; overflow: hidden; } .scroller { position: relative; width: 100%; height: 100%; overflow: hidden; background: #ffffff; } .track { display: inline-flex; align-items: center; gap: 48px; height: 100%; white-space: nowrap; will-change: transform; animation: scroll linear infinite; animation-duration: var(--scroll-duration, 40s); } .item { height: 60px; width: auto; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .item img { height: 30%; width: auto; max-width: 280px; min-width: 60px; object-fit: contain; display: block; filter: none; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; -webkit-backface-visibility: hidden; backface-visibility: hidden; transform: translateZ(0); } @keyframes scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } } .qr-float { position: fixed; right: 16px; bottom: 100px; width: 160px; height: 160px; background: #ffffff; border: 1px solid #e5e7eb; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.22); display: grid; place-items: center; opacity: 0; visibility: hidden; transform: translateY(10px) scale(0.96); transition: opacity .35s ease, transform .35s ease, visibility 0s linear .35s; z-index: 9999; } .qr-float.show { opacity: 1; visibility: visible; transform: translateY(0) scale(1); transition: opacity .35s ease, transform .35s ease, visibility 0s; } .qr-float img { max-width: 88%; max-height: 88%; object-fit: contain; display: block; } `; const OverlaySponsorsPage: React.FC = () => { const bg = useColorModeValue('transparent', 'transparent'); const { data: sponsors, isLoading } = useQuery({ queryKey: ['public-sponsors-list'], queryFn: listSponsorsPublic, refetchInterval: 60000, staleTime: 30000, }); const list = useMemo(() => Array.isArray(sponsors) ? sponsors.slice(0, 80) : [], [sponsors]); const trackRef = useRef(null); const containerRef = useRef(null); const [duration, setDuration] = useState(40); const [qrUrl, setQrUrl] = useState(''); const [qrVisible, setQrVisible] = useState(false); const scheduleRef = useRef<{ intId?: any } | null>(null); useEffect(() => { if (!trackRef.current) return; // After images load, compute total width and set duration const imgs = Array.from(trackRef.current.querySelectorAll('img')) as HTMLImageElement[]; if (imgs.length === 0) { setDuration(40); return; } let completed = 0; const check = () => { completed++; if (completed >= imgs.length) { // compute combined width of first half (since content duplicated) const children = Array.from(trackRef.current!.children) as HTMLElement[]; const half = Math.floor(children.length / 2); let total = 0; const gap = 48; for (let i = 0; i < half; i++) { const el = children[i]; if (el && (el as HTMLElement).style.display !== 'none') total += el.getBoundingClientRect().width; } const visible = half; // approximate if (visible > 0) total += (visible - 1) * gap; const halfWidth = total; // track anim scrolls by half const pps = 60; // pixels per second const d = halfWidth > 0 ? Math.max(15, halfWidth / pps) : 40; setDuration(Math.round(d)); } }; imgs.forEach((img) => { if (img.complete && img.naturalWidth > 0) check(); else { img.addEventListener('load', check, { once: true }); img.addEventListener('error', () => { const parent = img.parentElement as HTMLElement | null; if (parent) parent.style.display = 'none'; check(); }, { once: true }); } }); return () => { imgs.forEach((img) => { img.onload = null; img.onerror = null; }); }; }, [list]); useEffect(() => { let mounted = true; (async () => { try { const qr = await getQr(); if (mounted && qr) setQrUrl(qr); } catch {} try { const st = await getPublicScoreboard(); const everyMin = Math.max(1, Number(st.qrEvery || st.qrEvery === 0 ? st.qrEvery : (st as any).QRShowEveryMinutes || 5)); const durSec = Math.max(5, Number(st.qrDuration || st.qrDuration === 0 ? st.qrDuration : (st as any).QRShowDurationSeconds || 60)); // First show shortly after load, then on schedule const show = () => { setQrVisible(true); window.setTimeout(() => setQrVisible(false), durSec * 1000); }; window.setTimeout(show, 2500); scheduleRef.current = { intId: window.setInterval(show, everyMin * 60 * 1000) }; } catch {} })(); return () => { mounted = false; if (scheduleRef.current?.intId) window.clearInterval(scheduleRef.current.intId); }; }, []); return ( {isLoading ? (
) : ( <>
{/* duplicate content for seamless loop */} {list.concat(list).map((src, i) => (
{ (e.currentTarget.parentElement as HTMLElement).style.display='none'; }} />
))}
{qrUrl ? (
QR
) : null} )}
); }; export default OverlaySponsorsPage;