aboutsummaryrefslogtreecommitdiff
path: root/jb-ui/src/components/EventMarquee.tsx
diff options
context:
space:
mode:
authorMax Nanis2026-09-13 19:03:55 +0000
committerMax Nanis2026-09-13 19:03:55 +0000
commit8fbe8d439b418796932aaa88297e006c714e4d13 (patch)
tree6c800476edc1e771fc570b559d483df8d59d4324 /jb-ui/src/components/EventMarquee.tsx
parent12f6fee851b68e86af658dfa17e4a0daed457dd1 (diff)
parent2c94f248d2438071a918fa9a30bf114ef9aa29b4 (diff)
downloadamt-jb-8fbe8d439b418796932aaa88297e006c714e4d13.tar.gz
amt-jb-8fbe8d439b418796932aaa88297e006c714e4d13.zip
Merges pull request #3
Off of Amazon!!!
Diffstat (limited to 'jb-ui/src/components/EventMarquee.tsx')
-rw-r--r--jb-ui/src/components/EventMarquee.tsx428
1 files changed, 224 insertions, 204 deletions
diff --git a/jb-ui/src/components/EventMarquee.tsx b/jb-ui/src/components/EventMarquee.tsx
index 4ce4f15..46508ff 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 (
- <>
- <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 payload = event.payload as TaskEnterPayload;
+
+ return (
+ <>
+ <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 (
- <>
- <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>
- </>
- )
-}
+ const payload = event.payload as TaskFinishPayload;
+
+ return (
+ <>
+ <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>
+ </>
+ );
+};
const EventUserCreated = ({ event }: EventItemProps) => {
-
- return (
- <>
- <span className="px-2 text-lg">{getUnicodeFlagIcon(event.payload.country_iso)}</span>
- <p>{truncate(event.product_user_id!, 6, "*****")} Created</p>
- </>
- )
-}
-
+ return (
+ <>
+ <span className="px-2 text-lg">
+ {getUnicodeFlagIcon(event.payload.country_iso)}
+ </span>
+ <p>{truncate(event.product_user_id!, 6, "*****")} Created</p>
+ </>
+ );
+};
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></>
- }
+ 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]
- font-mono px-2">
- {renderContent()}
-
- {/* <span className="text-red-500 pl-5">◆</span> */}
- </div >
- )
-}
+ return (
+ <div
+ key={event.event_uuid!}
+ className="inline-flex items-center text-[10px]
+ font-mono px-2"
+ >
+ {renderContent()}
+ {/* <span className="text-red-500 pl-5">◆</span> */}
+ </div>
+ );
+};
// ─── 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<HTMLDivElement | null>(null); // The ticker band
- const stripRef = useRef<HTMLDivElement | null>(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<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;
-
- 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 (
- <div style={{
- 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,
- }}
- >
- {scrollItems.map(item => (
- <EventComponent event={item} />
- ))}
-
- {/* Trailing spacer so new content doesn't immediately snap in */}
- <span style={{ paddingRight: "100vw", display: "inline-block" }} />
-
- </div>
- </div>
-
- </div>
+ // --- Redux ---
+ const reduxItems = useSelector(selectAllEvents);
+ const targetSpeedRedux = useSelector(selectCurrentSpeed); // px/sec
+
+ // --- 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(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;
+
+ 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 (
+ <div
+ style={{
+ 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,
+ }}
+ >
+ {scrollItems.map((item) => (
+ <EventComponent event={item} />
+ ))}
+
+ {/* Trailing spacer so new content doesn't immediately snap in */}
+ <span style={{ paddingRight: "100vw", display: "inline-block" }} />
+ </div>
</div>
- );
+ </div>
+ </div>
+ );
}