import { EventEnvelope, EventType } from "@/api_fsb"; import { selectLatestEvent, selectLatestEvents } from "@/models/grlEventsSlice"; import { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; // ─── Config ─────────────────────────────────────────────────────────────────── const BASE_SPEED = 60; // px/sec at idle const MAX_SPEED = 380; // px/sec at surge const RATE_WINDOW = 6_000; // ms window to measure message frequency const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) const RATE_TO_SPEED = 55; // px/sec added per msg/sec interface EventItemProps { event: EventEnvelope; } const EventTaskEnter = ({ event }: EventItemProps) => { return ( <>

{event.product_user_id} Entered

) } const EventTaskFinish = ({ event }: EventItemProps) => { return ( <>

{event.product_user_id} Finished

) } const EventUserCreated = ({ event }: EventItemProps) => { return ( <>

{event.product_user_id} Created

) } const EventComponent = ({ event }: EventItemProps) => { const renderContent = () => { switch (event.event_type) { case EventType.TaskEnter: return case EventType.TaskFinish: return case EventType.UserCreated: return default: return <>

Unknown event

} } return (
{renderContent()}
) } // ─── Main Component ─────────────────────────────────────────────────────────── export default function NewsTicker() { const latestEvent = useSelector(selectLatestEvent); const latestEvents = useSelector(selectLatestEvents(10)); const itemsRef = useRef(latestEvents); const [speed, setSpeed] = useState(BASE_SPEED); const stripRef = useRef(null); const containerRef = useRef(null); const translateX = useRef(0); const currentSpeed = useRef(BASE_SPEED); const targetSpeed = useRef(BASE_SPEED); const rafRef = useRef(null); const lastTime = useRef(null); const nextId = useRef(1); const pendingQueue = useRef([]); const messageTimes = useRef([]); // keep itemsRef in sync useEffect(() => { console.log("Adding message to ticker:", latestEvent); itemsRef.current = latestEvents; }, [latestEvents]); // const addMessage = useCallback(() => { // console.log("Adding message to ticker:", latestEvent); // // calc rate over window // messageTimes.current = messageTimes.current.filter( // (t) => now - t < RATE_WINDOW // ); // // const rate = messageTimes.current.length / (RATE_WINDOW / 1000); // msgs/sec // // targetSpeed.current = Math.min(BASE_SPEED + rate * RATE_TO_SPEED, MAX_SPEED); // // pendingQueue.current.push({ id: nextId.current++, text }); // }, [latestEvent]); // RAF animation loop // useEffect(() => { // const loop = (timestamp) => { // if (!lastTime.current) lastTime.current = timestamp; // const delta = (timestamp - lastTime.current) / 1000; // seconds // lastTime.current = timestamp; // // ease speed toward target // currentSpeed.current += // (targetSpeed.current - currentSpeed.current) * SPEED_EASING; // // consume pending queue // if (pendingQueue.current.length > 0) { // const newItems = pendingQueue.current.splice(0); // setItems((prev) => [...prev, ...newItems]); // } // // advance strip // translateX.current -= currentSpeed.current * delta; // // apply transform directly — no re-render // if (stripRef.current) { // stripRef.current.style.transform = `translateX(${translateX.current}px)`; // } // // update speed display periodically (for the indicator) // setSpeed(Math.round(currentSpeed.current)); // // prune items that have scrolled off-screen // if (stripRef.current && containerRef.current) { // const children = Array.from(stripRef.current.children); // const containerLeft = containerRef.current.getBoundingClientRect().left; // let pruneCount = 0; // for (const child of children) { // const rect = child.getBoundingClientRect(); // if (rect.right < containerLeft - 20) { // pruneCount++; // } else { // break; // } // } // if (pruneCount > 0) { // // measure how much width we're removing so we can adjust translateX // let removedWidth = 0; // for (let i = 0; i < pruneCount; i++) { // removedWidth += children[i].getBoundingClientRect().width; // } // translateX.current += removedWidth; // setItems((prev) => prev.slice(pruneCount)); // } // } // rafRef.current = requestAnimationFrame(loop); // }; // rafRef.current = requestAnimationFrame(loop); // return () => cancelAnimationFrame(rafRef.current); // }, []); // speed bucket for visual indicator // const speedLevel = // currentSpeed.current < 100 ? 0 : // currentSpeed.current < 180 ? 1 : // currentSpeed.current < 260 ? 2 : 3; // const speedLabels = ["NORMAL", "ELEVATED", "HIGH", "SURGE"]; // const speedColors = ["#22d3ee", "#f59e0b", "#f97316", "#ef4444"]; return (
{/* Ticker wrapper */}
{/* The ticker band */}
{/* Scrolling strip */}
{latestEvents.map((item, i) => ( ))} {/* Trailing spacer so new content doesn't immediately snap in */}
); }