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 { isDesktop } from "react-device-detect"; import { HoverCard, HoverCardContent, HoverCardTrigger, } from "../ui/hover-card"; import { HoverCardPortal } from "@radix-ui/react-hover-card"; 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 = { 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", ...(isDesktop && { month: "short", day: "2-digit" }), })}
)} {isLastSegmentInMinimap && (
{new Date(alignedMinimapEndTime * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", ...(isDesktop && { month: "short", day: "2-digit" }), })}
)} ); } function Tick({ timestamp, timestampSpread }: TickSegmentProps) { return (
); } 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 { alignStartDateToTimeline, alignEndDateToTimeline } = 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 alignStartDateToTimeline(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( () => alignStartDateToTimeline(minimapStartTime ?? 0), [minimapStartTime, alignStartDateToTimeline] ); const alignedMinimapEndTime = useMemo( () => alignEndDateToTimeline(minimapEndTime ?? 0), [minimapEndTime, alignEndDateToTimeline] ); const isInMinimapRange = useMemo(() => { return ( showMinimap && segmentTime >= alignedMinimapStartTime && segmentTime < alignedMinimapEndTime ); }, [ showMinimap, alignedMinimapStartTime, alignedMinimapEndTime, 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 = `h-2 relative w-full ${ showMinimap ? isInMinimapRange ? "bg-card" : isLastSegmentInMinimap ? "" : "opacity-70" : "" } ${ isFirstSegmentInMinimap || isLastSegmentInMinimap ? "relative h-2 border-b-2 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;