From dec4f45c0755f65a322f6c66833c711dec2c6abb Mon Sep 17 00:00:00 2001
From: Max Nanis
Date: Mon, 2 Mar 2026 06:33:59 -0500
Subject: grlEventsReducers, EventMarquee, and workin on calc speed and
animations from redux (rather than example's array)
---
jb-ui/src/components/EventMarquee.tsx | 233 ++++++++++++++++++++++++++++++++++
1 file changed, 233 insertions(+)
create mode 100644 jb-ui/src/components/EventMarquee.tsx
(limited to 'jb-ui/src/components')
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 (
+ <>
+
{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 */}
+
+
+
+
+
+
+
+
+
+ );
+}
--
cgit v1.2.3