import { useApiHost } from "@/api"; import { useEventUtils } from "@/hooks/use-event-utils"; import { useSegmentUtils } from "@/hooks/use-segment-utils"; import { ReviewSegment, ReviewSeverity } from "@/types/review"; import React, { RefObject, useCallback, useEffect, useMemo, useRef, } from "react"; import { Tooltip, TooltipContent } from "../ui/tooltip"; import { TooltipTrigger } from "@radix-ui/react-tooltip"; type EventSegmentProps = { events: ReviewSegment[]; segmentTime: number; segmentDuration: number; timestampSpread: number; showMinimap: boolean; minimapStartTime?: number; minimapEndTime?: number; severityType: ReviewSeverity; contentRef: RefObject; }; type MinimapSegmentProps = { isFirstSegmentInMinimap: boolean; isLastSegmentInMinimap: boolean; alignedMinimapStartTime: number; alignedMinimapEndTime: number; firstMinimapSegmentRef: React.MutableRefObject; }; type TickSegmentProps = { isFirstSegmentInMinimap: boolean; isLastSegmentInMinimap: boolean; timestamp: Date; timestampSpread: number; }; type TimestampSegmentProps = { isFirstSegmentInMinimap: boolean; isLastSegmentInMinimap: boolean; timestamp: Date; timestampSpread: number; segmentKey: number; }; function MinimapBounds({ isFirstSegmentInMinimap, isLastSegmentInMinimap, alignedMinimapStartTime, alignedMinimapEndTime, firstMinimapSegmentRef, }: MinimapSegmentProps) { return ( <> {isFirstSegmentInMinimap && (
{new Date(alignedMinimapStartTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", month: "short", day: "2-digit", })}
)} {isLastSegmentInMinimap && (
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", month: "short", day: "2-digit", })}
)} ); } function Tick({ isFirstSegmentInMinimap, isLastSegmentInMinimap, timestamp, timestampSpread, }: TickSegmentProps) { return (
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
)}
); } function Timestamp({ isFirstSegmentInMinimap, isLastSegmentInMinimap, timestamp, timestampSpread, segmentKey, }: TimestampSegmentProps) { return (
{!isFirstSegmentInMinimap && !isLastSegmentInMinimap && (
{timestamp.getMinutes() % timestampSpread === 0 && timestamp.getSeconds() === 0 && timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })}
)}
); } export function EventSegment({ events, segmentTime, segmentDuration, timestampSpread, showMinimap, minimapStartTime, minimapEndTime, severityType, contentRef, }: EventSegmentProps) { const { getSeverity, getReviewed, displaySeverityType, shouldShowRoundedCorners, getEventStart, getEventThumbnail, } = useSegmentUtils(segmentDuration, events, severityType); const { alignDateToTimeline } = useEventUtils(events, segmentDuration); const severity = useMemo( () => getSeverity(segmentTime, displaySeverityType), [getSeverity, segmentTime] ); const reviewed = useMemo( () => getReviewed(segmentTime), [getReviewed, segmentTime] ); const { roundTopPrimary, roundBottomPrimary, roundTopSecondary, roundBottomSecondary, } = useMemo( () => shouldShowRoundedCorners(segmentTime), [shouldShowRoundedCorners, segmentTime] ); const startTimestamp = useMemo(() => { const eventStart = getEventStart(segmentTime); if (eventStart) { return alignDateToTimeline(eventStart); } }, [getEventStart, segmentTime]); const apiHost = useApiHost(); const eventThumbnail = useMemo(() => { return getEventThumbnail(segmentTime); }, [getEventThumbnail, segmentTime]); const timestamp = useMemo(() => new Date(segmentTime * 1000), [segmentTime]); const segmentKey = useMemo(() => segmentTime, [segmentTime]); const alignedMinimapStartTime = useMemo( () => alignDateToTimeline(minimapStartTime ?? 0), [minimapStartTime, alignDateToTimeline] ); const alignedMinimapEndTime = useMemo( () => alignDateToTimeline(minimapEndTime ?? 0), [minimapEndTime, alignDateToTimeline] ); const isInMinimapRange = useMemo(() => { return ( showMinimap && minimapStartTime && minimapEndTime && segmentTime > minimapStartTime && segmentTime < minimapEndTime ); }, [showMinimap, minimapStartTime, minimapEndTime, segmentTime]); const isFirstSegmentInMinimap = useMemo(() => { return showMinimap && segmentTime === alignedMinimapStartTime; }, [showMinimap, segmentTime, alignedMinimapStartTime]); const isLastSegmentInMinimap = useMemo(() => { return showMinimap && segmentTime === alignedMinimapEndTime; }, [showMinimap, segmentTime, alignedMinimapEndTime]); const firstMinimapSegmentRef = useRef(null); let debounceTimer: ReturnType; function debounceScrollIntoView(element: HTMLElement) { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { element.scrollIntoView({ behavior: "smooth", block: "center" }); }, 100); } useEffect(() => { // Check if the first segment is out of view const firstSegment = firstMinimapSegmentRef.current; if (firstSegment && showMinimap && isFirstSegmentInMinimap) { debounceScrollIntoView(firstSegment); } }, [showMinimap, isFirstSegmentInMinimap, events, segmentDuration]); const segmentClasses = `flex flex-row ${ showMinimap ? (isInMinimapRange ? "bg-muted" : "bg-background") : "" } ${ isFirstSegmentInMinimap || isLastSegmentInMinimap ? "relative h-2 border-b border-gray-500" : "" }`; const severityColors: { [key: number]: string } = { 1: reviewed ? "from-severity_motion-dimmed/50 to-severity_motion/50" : "from-severity_motion-dimmed to-severity_motion", 2: reviewed ? "from-severity_detection-dimmed/50 to-severity_detection/50" : "from-severity_detection-dimmed to-severity_detection", 3: reviewed ? "from-severity_alert-dimmed/50 to-severity_alert/50" : "from-severity_alert-dimmed to-severity_alert", }; const segmentClick = useCallback(() => { if (contentRef.current && startTimestamp) { const element = contentRef.current.querySelector( `[data-segment-start="${startTimestamp - segmentDuration}"]` ); if (element instanceof HTMLElement) { debounceScrollIntoView(element); element.classList.add( `outline-severity_${severityType}`, `shadow-severity_${severityType}` ); element.classList.add("outline-4", "shadow-[0_0_6px_1px]"); element.classList.remove("outline-0", "shadow-none"); // Remove the classes after a short timeout setTimeout(() => { element.classList.remove("outline-4", "shadow-[0_0_6px_1px]"); element.classList.add("outline-0", "shadow-none"); }, 3000); } } }, [startTimestamp]); return (
{severity.map((severityValue, index) => ( {severityValue === displaySeverityType && (
)} {severityValue !== displaySeverityType && (
)}
))}
); } export default EventSegment;