From 2c3f782e5e159f3b515bc29c8b8aa9efd227cbe1 Mon Sep 17 00:00:00 2001 From: mertbagt Date: Wed, 21 Jan 2026 07:37:55 -0500 Subject: [PATCH 1/8] basic framework to add timestamps to url --- components/hearing/HearingDetails.tsx | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index f79bdde5c..21dc1b82d 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -15,6 +15,8 @@ import { HearingSidebar } from "./HearingSidebar" import { HearingData, Paragraph, fetchTranscriptionData } from "./hearing" import { Transcriptions } from "./Transcriptions" +import { useRouter } from "next/router" + const LegalContainer = styled(Container)` background-color: white; ` @@ -63,6 +65,59 @@ export const HearingDetails = ({ videoRef.current ? (videoRef.current.currentTime = value) : null } + const router = useRouter() + + console.log("vref current: ", videoRef.current) + + const updateUrlWithTimestamp = () => { + if (videoRef.current) { + const timeInSeconds = Math.floor(videoRef.current.currentTime) + console.log("TIS: ", timeInSeconds) + router.push(`${hearingId}?t=${timeInSeconds}`, undefined, { + shallow: true + }) + } + } + + useEffect(() => { + videoRef.current + ? videoRef.current.addEventListener("pause", updateUrlWithTimestamp) + : null + + return () => { + videoRef.current + ? videoRef.current.removeEventListener("pause", updateUrlWithTimestamp) + : null + } + }, [videoRef.current]) + + useEffect(() => { + const startTime = router.query.t + + const convertToString = (value: string | string[] | undefined): string => { + if (Array.isArray(value)) { + return value.join(", ") + } + return value ?? "" + } + + const resultString: string = convertToString(startTime) + + console.log("result string", parseInt(resultString, 10)) + + if (startTime && videoRef.current) { + // if (startTime && videoRef.current) { + console.log("test 3") + setCurTimeVideo(parseInt(resultString, 10)) + // Wait for video metadata to load before seeking + videoRef.current.addEventListener("loadedmetadata", () => { + // if (videoRef.current !== null) + // videoRef.current.currentTime = parseInt(resultString, 10) + // console.log("test 2", videoRef.current.currentTime) + }) + } + }, [router.query.t, videoRef.current]) + useEffect(() => { ;(async function () { if (!videoTranscriptionId || transcriptData !== null) return From 9905da18231a322d1bf10177d7c4dd1e2be822a1 Mon Sep 17 00:00:00 2001 From: mertbagt Date: Wed, 21 Jan 2026 07:47:46 -0500 Subject: [PATCH 2/8] cleanup --- components/hearing/HearingDetails.tsx | 26 +++++--------------------- pages/hearing/[hearingId].tsx | 1 - 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index 21dc1b82d..c51ed5850 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -1,9 +1,8 @@ -import { doc, getDoc } from "firebase/firestore" +import { useRouter } from "next/router" import { Trans, useTranslation } from "next-i18next" -import { useCallback, useEffect, useRef, useState } from "react" +import { useEffect, useRef, useState } from "react" import styled from "styled-components" import { Col, Container, Image, Row } from "../bootstrap" -import { firestore } from "../firebase" import * as links from "../links" import { committeeURL, External } from "../links" import { @@ -15,8 +14,6 @@ import { HearingSidebar } from "./HearingSidebar" import { HearingData, Paragraph, fetchTranscriptionData } from "./hearing" import { Transcriptions } from "./Transcriptions" -import { useRouter } from "next/router" - const LegalContainer = styled(Container)` background-color: white; ` @@ -53,9 +50,11 @@ export const HearingDetails = ({ hearingData: HearingData }) => { const { t } = useTranslation(["common", "hearing"]) - const [transcriptData, setTranscriptData] = useState(null) + const router = useRouter() + const [transcriptData, setTranscriptData] = useState(null) const [videoLoaded, setVideoLoaded] = useState(false) + const handleVideoLoad = () => { setVideoLoaded(true) } @@ -65,14 +64,9 @@ export const HearingDetails = ({ videoRef.current ? (videoRef.current.currentTime = value) : null } - const router = useRouter() - - console.log("vref current: ", videoRef.current) - const updateUrlWithTimestamp = () => { if (videoRef.current) { const timeInSeconds = Math.floor(videoRef.current.currentTime) - console.log("TIS: ", timeInSeconds) router.push(`${hearingId}?t=${timeInSeconds}`, undefined, { shallow: true }) @@ -103,18 +97,8 @@ export const HearingDetails = ({ const resultString: string = convertToString(startTime) - console.log("result string", parseInt(resultString, 10)) - if (startTime && videoRef.current) { - // if (startTime && videoRef.current) { - console.log("test 3") setCurTimeVideo(parseInt(resultString, 10)) - // Wait for video metadata to load before seeking - videoRef.current.addEventListener("loadedmetadata", () => { - // if (videoRef.current !== null) - // videoRef.current.currentTime = parseInt(resultString, 10) - // console.log("test 2", videoRef.current.currentTime) - }) } }, [router.query.t, videoRef.current]) diff --git a/pages/hearing/[hearingId].tsx b/pages/hearing/[hearingId].tsx index 73a0ad6b7..84263d4e1 100644 --- a/pages/hearing/[hearingId].tsx +++ b/pages/hearing/[hearingId].tsx @@ -1,5 +1,4 @@ import { GetServerSideProps } from "next" -import { useRouter } from "next/router" import { serverSideTranslations } from "next-i18next/serverSideTranslations" import { z } from "zod" import { flags } from "components/featureFlags" From 1cc80cd1aedec28c70a560fa541a6a57c18e8559 Mon Sep 17 00:00:00 2001 From: mertbagt Date: Mon, 26 Jan 2026 15:17:51 -0500 Subject: [PATCH 3/8] copy to clipboard url button --- components/hearing/HearingDetails.tsx | 1 + components/hearing/Transcriptions.tsx | 27 +++++++++++++++++++++++++-- components/hearing/hearing.ts | 7 +++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index c51ed5850..e26bec78a 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -208,6 +208,7 @@ export const HearingDetails = ({ {transcriptData ? ( { @@ -249,12 +257,14 @@ export const Transcriptions = ({ const TranscriptItem = forwardRef(function TranscriptItem( { element, + hearingId, highlightedId, index, setCurTimeVideo, searchTerm }: { element: Paragraph + hearingId: string highlightedId: number index: number setCurTimeVideo: any @@ -316,6 +326,19 @@ const TranscriptItem = forwardRef(function TranscriptItem( {highlightText(element.text, searchTerm)} + + + + + ) }) diff --git a/components/hearing/hearing.ts b/components/hearing/hearing.ts index da809c6ee..4a2bf98f0 100644 --- a/components/hearing/hearing.ts +++ b/components/hearing/hearing.ts @@ -97,3 +97,10 @@ export function formatMilliseconds(ms: number): string { return `${formattedMinutes}:${formattedSeconds}` } } + +export function formatTotalSeconds(ms: number): string { + const totalSeconds = Math.floor(ms / 1000) + const formattedSeconds = String(totalSeconds) + + return `${formattedSeconds}` +} From 248ef5d8171519c9a8e448759182ba33d9bf69f1 Mon Sep 17 00:00:00 2001 From: mertbagt Date: Mon, 26 Jan 2026 17:37:01 -0500 Subject: [PATCH 4/8] handle page load --- components/hearing/Transcriptions.tsx | 36 ++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/components/hearing/Transcriptions.tsx b/components/hearing/Transcriptions.tsx index 1e1285340..e6db6787e 100644 --- a/components/hearing/Transcriptions.tsx +++ b/components/hearing/Transcriptions.tsx @@ -11,6 +11,8 @@ import { Col, Container, Row } from "../bootstrap" import { Paragraph, formatMilliseconds, formatTotalSeconds } from "./hearing" import { CopyButton } from "components/buttons" +import { useRouter } from "next/router" + const ClearButton = styled(FontAwesomeIcon)` position: absolute; right: 3rem; @@ -152,11 +154,38 @@ export const Transcriptions = ({ ) }, [transcriptData, searchTerm]) + const router = useRouter() + const startTime = router.query.t + const convertToString = (value: string | string[] | undefined): string => { + if (Array.isArray(value)) { + return value.join(", ") + } + return value ?? "" + } + + const resultString: string = convertToString(startTime) + + let currentIndex = transcriptData.findIndex( + element => parseInt(resultString, 10) <= element.end / 1000 + ) + + // this useEffect sets highlighter on inital page load + // the next useEffect handles general video usage + // both are needed + + useEffect(() => { + if (containerRef.current && currentIndex !== highlightedId) { + setHighlightedId(currentIndex) + } + }, [videoLoaded]) + useEffect(() => { const handleTimeUpdate = () => { - const currentIndex = transcriptData.findIndex( - element => videoRef.current.currentTime <= element.end / 1000 - ) + videoLoaded + ? (currentIndex = transcriptData.findIndex( + element => videoRef.current.currentTime <= element.end / 1000 + )) + : null if (containerRef.current && currentIndex !== highlightedId) { setHighlightedId(currentIndex) if (currentIndex !== -1 && !searchTerm) { @@ -285,6 +314,7 @@ const TranscriptItem = forwardRef(function TranscriptItem( const isHighlighted = (index: number): boolean => { return index === highlightedId } + const highlightText = (text: string, term: string) => { if (!term) return text const escaped = term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") From 9d5dfb0d951f5d41e3fc80ab70913fcc6b56fc8f Mon Sep 17 00:00:00 2001 From: mertbagt Date: Mon, 26 Jan 2026 18:51:01 -0500 Subject: [PATCH 5/8] remove unneeded code --- components/hearing/HearingDetails.tsx | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index e26bec78a..460784f56 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -64,27 +64,6 @@ export const HearingDetails = ({ videoRef.current ? (videoRef.current.currentTime = value) : null } - const updateUrlWithTimestamp = () => { - if (videoRef.current) { - const timeInSeconds = Math.floor(videoRef.current.currentTime) - router.push(`${hearingId}?t=${timeInSeconds}`, undefined, { - shallow: true - }) - } - } - - useEffect(() => { - videoRef.current - ? videoRef.current.addEventListener("pause", updateUrlWithTimestamp) - : null - - return () => { - videoRef.current - ? videoRef.current.removeEventListener("pause", updateUrlWithTimestamp) - : null - } - }, [videoRef.current]) - useEffect(() => { const startTime = router.query.t From 76b35dd1f40f9d73fc791e16dbb1ddf2c6262c77 Mon Sep 17 00:00:00 2001 From: mertbagt Date: Mon, 26 Jan 2026 18:58:42 -0500 Subject: [PATCH 6/8] set aside common function --- components/hearing/HearingDetails.tsx | 15 ++++++--------- components/hearing/Transcriptions.tsx | 17 +++++++---------- components/hearing/hearing.ts | 9 +++++++++ 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index 460784f56..e6e482d34 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -11,7 +11,12 @@ import { FeatureCalloutButton } from "../shared/CommonComponents" import { HearingSidebar } from "./HearingSidebar" -import { HearingData, Paragraph, fetchTranscriptionData } from "./hearing" +import { + HearingData, + Paragraph, + convertToString, + fetchTranscriptionData +} from "./hearing" import { Transcriptions } from "./Transcriptions" const LegalContainer = styled(Container)` @@ -66,14 +71,6 @@ export const HearingDetails = ({ useEffect(() => { const startTime = router.query.t - - const convertToString = (value: string | string[] | undefined): string => { - if (Array.isArray(value)) { - return value.join(", ") - } - return value ?? "" - } - const resultString: string = convertToString(startTime) if (startTime && videoRef.current) { diff --git a/components/hearing/Transcriptions.tsx b/components/hearing/Transcriptions.tsx index e6db6787e..40bcda0d0 100644 --- a/components/hearing/Transcriptions.tsx +++ b/components/hearing/Transcriptions.tsx @@ -4,15 +4,19 @@ import { faTimes } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { useRouter } from "next/router" import { useTranslation } from "next-i18next" import React, { forwardRef, useEffect, useRef, useState } from "react" import styled from "styled-components" import { Col, Container, Row } from "../bootstrap" -import { Paragraph, formatMilliseconds, formatTotalSeconds } from "./hearing" +import { + Paragraph, + convertToString, + formatMilliseconds, + formatTotalSeconds +} from "./hearing" import { CopyButton } from "components/buttons" -import { useRouter } from "next/router" - const ClearButton = styled(FontAwesomeIcon)` position: absolute; right: 3rem; @@ -156,13 +160,6 @@ export const Transcriptions = ({ const router = useRouter() const startTime = router.query.t - const convertToString = (value: string | string[] | undefined): string => { - if (Array.isArray(value)) { - return value.join(", ") - } - return value ?? "" - } - const resultString: string = convertToString(startTime) let currentIndex = transcriptData.findIndex( diff --git a/components/hearing/hearing.ts b/components/hearing/hearing.ts index 4a2bf98f0..512644bc6 100644 --- a/components/hearing/hearing.ts +++ b/components/hearing/hearing.ts @@ -29,6 +29,15 @@ export type Paragraph = { text: string } +export const convertToString = ( + value: string | string[] | undefined +): string => { + if (Array.isArray(value)) { + return value.join(", ") + } + return value ?? "" +} + export async function fetchHearingData( hearingId: string ): Promise { From 03b3c9312149af091123009c76f7bf57eb73d4a5 Mon Sep 17 00:00:00 2001 From: Mephistic Date: Tue, 27 Jan 2026 17:58:59 -0500 Subject: [PATCH 7/8] fix(transcripts): Make the auto-scroll work when first loading a Hearing page at a specific timestamp, some de-duping --- components/hearing/Transcriptions.tsx | 76 ++++++++++++++++++--------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/components/hearing/Transcriptions.tsx b/components/hearing/Transcriptions.tsx index 40bcda0d0..4590264be 100644 --- a/components/hearing/Transcriptions.tsx +++ b/components/hearing/Transcriptions.tsx @@ -145,11 +145,40 @@ export const Transcriptions = ({ const transcriptRefs = useRef(new Map()) const [searchTerm, setSearchTerm] = useState("") const [filteredData, setFilteredData] = useState([]) + const [initialScrollTarget, setInitialScrollTarget] = useState( + null + ) + const hasScrolledToInitial = useRef(false) const handleClearInput = () => { setSearchTerm("") } + // Shared function to scroll to a transcript index + const scrollToTranscript = (index: number) => { + const container = containerRef.current + const elem = transcriptRefs.current.get(index) + + if (elem && container) { + const elemTop = elem.offsetTop - container.offsetTop + const elemBottom = elemTop + elem.offsetHeight + const viewTop = container.scrollTop + const viewBottom = viewTop + container.clientHeight + + if (elemTop < viewTop) { + container.scrollTo({ + top: elemTop, + behavior: "smooth" + }) + } else if (elemBottom > viewBottom) { + container.scrollTo({ + top: elemBottom - container.clientHeight, + behavior: "smooth" + }) + } + } + } + useEffect(() => { setFilteredData( transcriptData.filter(el => @@ -166,15 +195,31 @@ export const Transcriptions = ({ element => parseInt(resultString, 10) <= element.end / 1000 ) - // this useEffect sets highlighter on inital page load - // the next useEffect handles general video usage - // both are needed + // Set the initial scroll target when we have a startTime and transcripts + useEffect(() => { + if ( + startTime && + transcriptData.length > 0 && + currentIndex !== -1 && + !hasScrolledToInitial.current + ) { + setInitialScrollTarget(currentIndex) + } + }, [startTime, transcriptData, currentIndex]) + // Scroll to the initial target when the ref becomes available useEffect(() => { - if (containerRef.current && currentIndex !== highlightedId) { - setHighlightedId(currentIndex) + if (initialScrollTarget !== null && !searchTerm) { + const elem = transcriptRefs.current.get(initialScrollTarget) + + if (elem) { + setHighlightedId(initialScrollTarget) + scrollToTranscript(initialScrollTarget) + hasScrolledToInitial.current = true + setInitialScrollTarget(null) + } } - }, [videoLoaded]) + }, [initialScrollTarget, transcriptRefs.current.size, searchTerm]) useEffect(() => { const handleTimeUpdate = () => { @@ -186,24 +231,7 @@ export const Transcriptions = ({ if (containerRef.current && currentIndex !== highlightedId) { setHighlightedId(currentIndex) if (currentIndex !== -1 && !searchTerm) { - const container = containerRef.current - const elem = transcriptRefs.current.get(currentIndex) - const elemTop = elem.offsetTop - container.offsetTop - const elemBottom = elemTop + elem.offsetHeight - const viewTop = container.scrollTop - const viewBottom = viewTop + container.clientHeight - - if (elemTop < viewTop) { - container.scrollTo({ - top: elemTop, - behavior: "smooth" - }) - } else if (elemBottom > viewBottom) { - container.scrollTo({ - top: elemBottom - container.clientHeight, - behavior: "smooth" - }) - } + scrollToTranscript(currentIndex) } } } From e6e0f9f2c303dcd6b752fec4fdda8059586c4d60 Mon Sep 17 00:00:00 2001 From: mertbagt Date: Tue, 27 Jan 2026 18:57:34 -0500 Subject: [PATCH 8/8] cr2 --- components/hearing/hearing.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/hearing/hearing.ts b/components/hearing/hearing.ts index bad794744..c87bba25c 100644 --- a/components/hearing/hearing.ts +++ b/components/hearing/hearing.ts @@ -112,11 +112,8 @@ export function formatTotalSeconds(ms: number): string { const formattedSeconds = String(totalSeconds) return `${formattedSeconds}` -<<<<<<< HEAD } -======= ->>>>>>> 5e54701fee2a979206b7bde3806e04e4ecd0e4bd export function formatVTTTimestamp(ms: number): string { const totalSeconds = Math.floor(ms / 1000) const milliseconds = ms % 1000