From 979e789c388e7a1e9a90e448d82e297c9c296a3e Mon Sep 17 00:00:00 2001
From: Max Nanis
Date: Tue, 3 Mar 2026 04:55:07 -0500
Subject: country-flag-icon, grlEventsSlice overhaul (ditch latestModel
 tracking for useRef list in Component).  speed calc in createSelector(),
 seperated components.

---
 jb-ui/src/components/EventMarquee.tsx | 235 +++++++++++++++++-----------------
 1 file changed, 116 insertions(+), 119 deletions(-)

(limited to 'jb-ui/src/components/EventMarquee.tsx')

diff --git a/jb-ui/src/components/EventMarquee.tsx b/jb-ui/src/components/EventMarquee.tsx
index 8664e3b..4ce4f15 100644
--- a/jb-ui/src/components/EventMarquee.tsx
+++ b/jb-ui/src/components/EventMarquee.tsx
@@ -1,33 +1,45 @@
-import { EventEnvelope, EventType } from "@/api_fsb";
-import { selectLatestEvent, selectLatestEvents } from "@/models/grlEventsSlice";
+import { EventEnvelope, EventType, TaskEnterPayload, TaskFinishPayload } from "@/api_fsb";
+import { formatSecondsVerbose, formatSource, formatStatus, truncate } from "@/lib/utils";
+import { selectAllEvents, selectCurrentSpeed } from "@/models/grlEventsSlice";
+import getUnicodeFlagIcon from 'country-flag-icons/unicode';
 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) => {
+    const payload = event.payload as TaskEnterPayload;
 
     return (
         <>
-            <h2 className="text-lg">{event.product_user_id} Entered</h2>
+            <span className="px-2 text-lg">{getUnicodeFlagIcon(payload.country_iso)}</span>
+            <p>
+                {truncate(event.product_user_id!, 6, "*****")} Entered{" "}
+                {formatSource(payload.source)}{" "}
+                (#{truncate(payload.survey_id!, 5, "***")})
+            </p>
         </>
     )
 }
 
 const EventTaskFinish = ({ event }: EventItemProps) => {
+    const payload = event.payload as TaskFinishPayload;
 
     return (
         <>
-            <h2 className="text-lg">{event.product_user_id} Finished</h2>
+            <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span>
+            <p>
+                {truncate(event.product_user_id!, 6, "*****")}{" "}
+                {formatStatus(payload.status)} {formatSource(payload.source)}{" "}
+                (#{truncate(payload.survey_id!, 5, "***")}) in{" "}
+                {formatSecondsVerbose(payload.duration_sec!)}
+            </p>
         </>
     )
 }
@@ -36,7 +48,8 @@ const EventUserCreated = ({ event }: EventItemProps) => {
 
     return (
         <>
-            <h2 className="text-lg">{event.product_user_id} Created</h2>
+            <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span>
+            <p>{truncate(event.product_user_id!, 6, "*****")} Created</p>
         </>
     )
 }
@@ -59,10 +72,11 @@ const EventComponent = ({ event }: EventItemProps) => {
 
     return (
         <div key={event.event_uuid!}
-            className="inline-flex items-center text-[10px]">
+            className="inline-flex items-center text-[10px]
+            font-mono px-2">
             {renderContent()}
 
-            <span className="text-red-500 pl-5">◆</span>
+            {/* <span className="text-red-500 pl-5">◆</span> */}
         </div >
     )
 }
@@ -70,116 +84,105 @@ const EventComponent = ({ event }: EventItemProps) => {
 
 // ─── Main Component ───────────────────────────────────────────────────────────
 export default function NewsTicker() {
-    const latestEvent = useSelector(selectLatestEvent);
-    const latestEvents = useSelector(selectLatestEvents(10));
-    const itemsRef = useRef(latestEvents);
-
+    // --- Redux ---
+    const reduxItems = useSelector(selectAllEvents);
+    const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec
 
-    const [speed, setSpeed] = useState(BASE_SPEED);
-
-    const stripRef = useRef(null);
-    const containerRef = useRef(null);
+    // --- Scroll state (all refs — no re-renders from animation) ---
+    const containerRef = useRef<HTMLDivElement | null>(null);  // The ticker band 
+    const stripRef = useRef<HTMLDivElement | null>(null);  // Scrolling strip
     const translateX = useRef(0);
-    const currentSpeed = useRef(BASE_SPEED);
-    const targetSpeed = useRef(BASE_SPEED);
-    const rafRef = useRef(null);
-    const lastTime = useRef(null);
-
+    const currentSpeed = useRef(targetSpeedRedux);
+    const targetSpeed = useRef(targetSpeedRedux);
+
+    // Only state and refs persist across renders, the rest are re-initialized,
+    // that is why this isn't just a standard variable.
+    // Request Animation Frame reference (stores the animation frame ID)
+    const rafRef = useRef<number | null>(null);
+    const lastTime = useRef<number | null>(null);
+
+    // --- Item tracking ---
+    // Track which IDs are already in the scroller to only append 
+    // genuinely new ones.
+    const seenUUIDs = useRef(new Set<string>());
+    const pendingQueue = useRef<EventEnvelope[]>([]);
+    const [scrollItems, setScrollItems] = useState<EventEnvelope[]>([]);
+
+    // --- Sync Redux → pendingQueue (only new items) ---
+    useEffect(() => {
+        const newItems = reduxItems.filter((item) => !seenUUIDs.current.has(item.event_uuid!));
+        if (newItems.length === 0) return;
 
-    const nextId = useRef(1);
-    const pendingQueue = useRef([]);
-    const messageTimes = useRef([]);
+        newItems.forEach(item => seenUUIDs.current.add(item.event_uuid!));
+        pendingQueue.current.push(...newItems);
+    }, [reduxItems]);
 
+    // --- Sync target speed from Redux ---
+    useEffect(() => {
+        targetSpeed.current = targetSpeedRedux;
+    }, [targetSpeedRedux]);
 
-    // keep itemsRef in sync
+    // --- RAF loop ---
     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"];
+        // This timestamp is a "high-resolution timestamp" provided by 
+        // the browser, which is more accurate for measuring frame intervals 
+        // than Date.now()
+        const loop = (timestamp: number) => {
+            console.log("RAF loop, timestamp:", timestamp, lastTime.current);
+
+            if (!lastTime.current) lastTime.current = timestamp;
+            const delta = (timestamp - lastTime.current) / 1000;
+            lastTime.current = timestamp;
+
+            // Slow easement towards targetSpeed
+            currentSpeed.current +=
+                (targetSpeed.current - currentSpeed.current) * SPEED_EASING;
+
+            // flush pending queue into React state
+            if (pendingQueue.current.length > 0) {
+                const incoming = pendingQueue.current.splice(0);
+                setScrollItems((prev) => [...prev, ...incoming]);
+            }
+
+            // Advance strip, slides it over left by X pixels
+            translateX.current -= currentSpeed.current * delta;
+            if (stripRef.current) {
+                stripRef.current.style.transform = `translateX(${translateX.current}px)`;
+            }
+
+            // prune items that have fully exited the left edge
+            if (stripRef.current && containerRef.current) {
+                const children = Array.from(stripRef.current.children);
+                const containerLeft = containerRef.current.getBoundingClientRect().left;
+                let pruneCount = 0;
+                let removedWidth = 0;
+
+                for (const child of children) {
+                    const rect = child.getBoundingClientRect();
+                    if (rect.right < containerLeft - 20) {
+                        pruneCount++;
+                        removedWidth += rect.width;
+                    } else {
+                        break;
+                    }
+                }
+
+                if (pruneCount > 0) {
+                    translateX.current += removedWidth;
+                    setScrollItems((prev) => prev.slice(pruneCount));
+                }
+            }
+
+            // Assign the next loop
+            rafRef.current = requestAnimationFrame(loop);
+        };
+
+        rafRef.current = requestAnimationFrame(loop);
+        return () => cancelAnimationFrame(rafRef.current!);
+    }, []);
 
     return (
         <div style={{
-            fontFamily: "'Courier New', monospace",
             minHeight: "3vh", display: "flex", flexDirection: "column",
             justifyContent: "center", alignItems: "center", gap: "0"
         }}>
@@ -210,7 +213,7 @@ export default function NewsTicker() {
                             zIndex: 2,
                         }}
                     >
-                        {latestEvents.map((item, i) => (
+                        {scrollItems.map(item => (
                             <EventComponent event={item} />
                         ))}
 
@@ -222,12 +225,6 @@ export default function NewsTicker() {
 
             </div>
 
-            <style>{`
-        @keyframes pulse {
-          0%, 100% { opacity: 1; }
-          50% { opacity: 0.3; }
-        }
-      `}</style>
         </div>
     );
 }
-- 
cgit v1.2.3

