diff options
Diffstat (limited to 'jb-ui/src/components/EventMarquee.tsx')
| -rw-r--r-- | jb-ui/src/components/EventMarquee.tsx | 233 |
1 files changed, 233 insertions, 0 deletions
diff --git a/jb-ui/src/components/EventMarquee.tsx b/jb-ui/src/components/EventMarquee.tsx new file mode 100644 index 0000000..8664e3b --- /dev/null +++ b/jb-ui/src/components/EventMarquee.tsx @@ -0,0 +1,233 @@ +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 ( + <> + <h2 className="text-lg">{event.product_user_id} Entered</h2> + </> + ) +} + +const EventTaskFinish = ({ event }: EventItemProps) => { + + return ( + <> + <h2 className="text-lg">{event.product_user_id} Finished</h2> + </> + ) +} + +const EventUserCreated = ({ event }: EventItemProps) => { + + return ( + <> + <h2 className="text-lg">{event.product_user_id} Created</h2> + </> + ) +} + + +const EventComponent = ({ event }: EventItemProps) => { + + const renderContent = () => { + switch (event.event_type) { + case EventType.TaskEnter: + return <EventTaskEnter event={event} /> + case EventType.TaskFinish: + return <EventTaskFinish event={event} /> + case EventType.UserCreated: + return <EventUserCreated event={event} /> + default: + return <><p>Unknown event</p></> + } + } + + return ( + <div key={event.event_uuid!} + className="inline-flex items-center text-[10px]"> + {renderContent()} + + <span className="text-red-500 pl-5">◆</span> + </div > + ) +} + + +// ─── 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 ( + <div style={{ + fontFamily: "'Courier New', monospace", + minHeight: "3vh", display: "flex", flexDirection: "column", + justifyContent: "center", alignItems: "center", gap: "0" + }}> + + {/* Ticker wrapper */} + <div style={{ width: "100%", position: "relative" }}> + + {/* The ticker band */} + <div + ref={containerRef} + style={{ + overflow: "hidden", + padding: "0", + position: "relative", + }} + > + + {/* Scrolling strip */} + <div + ref={stripRef} + style={{ + display: "inline-flex", + alignItems: "center", + whiteSpace: "nowrap", + padding: "10px 0", + willChange: "transform", + position: "relative", + zIndex: 2, + }} + > + {latestEvents.map((item, i) => ( + <EventComponent event={item} /> + ))} + + {/* Trailing spacer so new content doesn't immediately snap in */} + <span style={{ paddingRight: "100vw", display: "inline-block" }} /> + + </div> + </div> + + </div> + + <style>{` + @keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.3; } + } + `}</style> + </div> + ); +} |
