From ed201936813d9a720b51e6cb40a05267b94e7314 Mon Sep 17 00:00:00 2001 From: Max Nanis Date: Thu, 10 Sep 2026 17:46:41 -0700 Subject: Typescript prettier Committing seperate so it's easier to see actual changes. --- jb-ui/src/components/EventMarquee.tsx | 428 ++++++++++++++++++---------------- 1 file changed, 224 insertions(+), 204 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 4ce4f15..6aab3f8 100644 --- a/jb-ui/src/components/EventMarquee.tsx +++ b/jb-ui/src/components/EventMarquee.tsx @@ -1,230 +1,250 @@ -import { EventEnvelope, EventType, TaskEnterPayload, TaskFinishPayload } from "@/api_fsb"; -import { formatSecondsVerbose, formatSource, formatStatus, truncate } from "@/lib/utils"; +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 getUnicodeFlagIcon from "country-flag-icons/unicode"; import { useEffect, useRef, useState } from "react"; import { useSelector } from "react-redux"; // ─── Config ─────────────────────────────────────────────────────────────────── -const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) +const SPEED_EASING = 0.025; // how quickly speed transitions (per frame) interface EventItemProps { - event: EventEnvelope; + event: EventEnvelope; } const EventTaskEnter = ({ event }: EventItemProps) => { - const payload = event.payload as TaskEnterPayload; - - return ( - <> - {getUnicodeFlagIcon(payload.country_iso)} -

- {truncate(event.product_user_id!, 6, "*****")} Entered{" "} - {formatSource(payload.source)}{" "} - (#{truncate(payload.survey_id!, 5, "***")}) -

- - ) -} + const payload = event.payload as TaskEnterPayload; + + return ( + <> + + {getUnicodeFlagIcon(payload.country_iso)} + +

+ {truncate(event.product_user_id!, 6, "*****")} Entered{" "} + {formatSource(payload.source)} (# + {truncate(payload.survey_id!, 5, "***")}) +

+ + ); +}; const EventTaskFinish = ({ event }: EventItemProps) => { - const payload = event.payload as TaskFinishPayload; - - return ( - <> - {getUnicodeFlagIcon(event.payload.country_iso)} -

- {truncate(event.product_user_id!, 6, "*****")}{" "} - {formatStatus(payload.status)} {formatSource(payload.source)}{" "} - (#{truncate(payload.survey_id!, 5, "***")}) in{" "} - {formatSecondsVerbose(payload.duration_sec!)} -

- - ) -} + const payload = event.payload as TaskFinishPayload; + + return ( + <> + + {getUnicodeFlagIcon(event.payload.country_iso)} + +

+ {truncate(event.product_user_id!, 6, "*****")}{" "} + {formatStatus(payload.status)} {formatSource(payload.source)} (# + {truncate(payload.survey_id!, 5, "***")}) in{" "} + {formatSecondsVerbose(payload.duration_sec!)} +

+ + ); +}; const EventUserCreated = ({ event }: EventItemProps) => { - - return ( - <> - {getUnicodeFlagIcon(event.payload.country_iso)} -

{truncate(event.product_user_id!, 6, "*****")} Created

- - ) -} - + return ( + <> + + {getUnicodeFlagIcon(event.payload.country_iso)} + +

{truncate(event.product_user_id!, 6, "*****")} 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

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

Unknown event

+ + ); } + }; - return ( -
- {renderContent()} - - {/* ◆ */} -
- ) -} + return ( +
+ {renderContent()} + {/* ◆ */} +
+ ); +}; // ─── Main Component ─────────────────────────────────────────────────────────── export default function NewsTicker() { - // --- Redux --- - const reduxItems = useSelector(selectAllEvents); - const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec - - // --- Scroll state (all refs — no re-renders from animation) --- - const containerRef = useRef(null); // The ticker band - const stripRef = useRef(null); // Scrolling strip - const translateX = useRef(0); - 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(null); - const lastTime = useRef(null); - - // --- Item tracking --- - // Track which IDs are already in the scroller to only append - // genuinely new ones. - const seenUUIDs = useRef(new Set()); - const pendingQueue = useRef([]); - const [scrollItems, setScrollItems] = useState([]); - - // --- Sync Redux → pendingQueue (only new items) --- - useEffect(() => { - const newItems = reduxItems.filter((item) => !seenUUIDs.current.has(item.event_uuid!)); - if (newItems.length === 0) return; - - newItems.forEach(item => seenUUIDs.current.add(item.event_uuid!)); - pendingQueue.current.push(...newItems); - }, [reduxItems]); - - // --- Sync target speed from Redux --- - useEffect(() => { - targetSpeed.current = targetSpeedRedux; - }, [targetSpeedRedux]); - - // --- RAF loop --- - useEffect(() => { - // 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 ( -
- - {/* Ticker wrapper */} -
- - {/* The ticker band */} -
- - {/* Scrolling strip */} -
- {scrollItems.map(item => ( - - ))} - - {/* Trailing spacer so new content doesn't immediately snap in */} - - -
-
- -
+ // --- Redux --- + const reduxItems = useSelector(selectAllEvents); + const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec + + // --- Scroll state (all refs — no re-renders from animation) --- + const containerRef = useRef(null); // The ticker band + const stripRef = useRef(null); // Scrolling strip + const translateX = useRef(0); + 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(null); + const lastTime = useRef(null); + + // --- Item tracking --- + // Track which IDs are already in the scroller to only append + // genuinely new ones. + const seenUUIDs = useRef(new Set()); + const pendingQueue = useRef([]); + const [scrollItems, setScrollItems] = useState([]); + + // --- Sync Redux → pendingQueue (only new items) --- + useEffect(() => { + const newItems = reduxItems.filter( + (item) => !seenUUIDs.current.has(item.event_uuid!), + ); + if (newItems.length === 0) return; + + newItems.forEach((item) => seenUUIDs.current.add(item.event_uuid!)); + pendingQueue.current.push(...newItems); + }, [reduxItems]); + + // --- Sync target speed from Redux --- + useEffect(() => { + targetSpeed.current = targetSpeedRedux; + }, [targetSpeedRedux]); + + // --- RAF loop --- + useEffect(() => { + // 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 ( +
+ {/* Ticker wrapper */} +
+ {/* The ticker band */} +
+ {/* Scrolling strip */} +
+ {scrollItems.map((item) => ( + + ))} + + {/* Trailing spacer so new content doesn't immediately snap in */} + +
- ); +
+
+ ); } -- cgit v1.2.3