@@ -2,7 +2,8 @@ import { useRouter } from "next/router"
22import { Trans , useTranslation } from "next-i18next"
33import { useEffect , useRef , useState } from "react"
44import styled from "styled-components"
5- import { Col , Container , Image , Row } from "../bootstrap"
5+ import { ButtonGroup } from "react-bootstrap"
6+ import { Col , Container , Image , Row , Button } from "../bootstrap"
67import * as links from "../links"
78import { committeeURL , External } from "../links"
89import {
@@ -14,8 +15,10 @@ import { HearingSidebar } from "./HearingSidebar"
1415import {
1516 HearingData ,
1617 Paragraph ,
18+ TranscriptData ,
1719 convertToString ,
18- fetchTranscriptionData
20+ fetchTranscriptionData ,
21+ toVTT
1922} from "./hearing"
2023import { Transcriptions } from "./Transcriptions"
2124
@@ -39,6 +42,34 @@ const VideoParent = styled.div`
3942 overflow: hidden;
4043`
4144
45+ const VideoButton = styled ( Button ) `
46+ border: none;
47+ background: transparent;
48+ color: ${ ( { $active } ) => ( $active ? "#212529" : "#6c757d" ) } ;
49+ font-weight: ${ ( { $active } ) => ( $active ? "600" : "500" ) } ;
50+ padding: 0.75rem 1rem;
51+ border-radius: 0;
52+ position: relative;
53+ transition: all 0.25s ease-in-out;
54+
55+ &:hover {
56+ color: #212529;
57+ background-color: rgba(0, 0, 0, 0.03);
58+ }
59+
60+ &::after {
61+ content: "";
62+ position: absolute;
63+ bottom: 0;
64+ left: 50%;
65+ width: ${ ( { $active } ) => ( $active ? "100%" : "0%" ) } ;
66+ height: 2px;
67+ background-color: #212529;
68+ transition: all 0.3s ease-in-out;
69+ transform: translateX(-50%);
70+ }
71+ `
72+
4273export const HearingDetails = ( {
4374 hearingData : {
4475 billsInAgenda,
@@ -48,21 +79,97 @@ export const HearingDetails = ({
4879 generalCourtNumber,
4980 hearingDate,
5081 hearingId,
51- videoTranscriptionId,
52- videoURL
82+ videos
5383 }
5484} : {
5585 hearingData : HearingData
5686} ) => {
5787 const { t } = useTranslation ( [ "common" , "hearing" ] )
5888 const router = useRouter ( )
89+ const previousActive = useRef < number | null > ( null )
90+ const routerReady = useRef ( false )
91+ const [ activeVideo , setActiveVideo ] = useState < number > ( 0 )
92+ const [ transcripts , setTranscripts ] = useState <
93+ ( TranscriptData | null ) [ ] | null
94+ > ( null )
5995
60- const [ transcriptData , setTranscriptData ] = useState < Paragraph [ ] | null > ( null )
61- const [ videoLoaded , setVideoLoaded ] = useState ( false )
96+ // Important this occurs before router check; otherwise time will be improperly removed on first render
97+ useEffect ( ( ) => {
98+ if (
99+ previousActive . current === null ||
100+ previousActive . current === activeVideo
101+ )
102+ return
103+ previousActive . current = activeVideo
104+ if ( activeVideo !== 0 ) {
105+ router . replace (
106+ {
107+ pathname : router . pathname ,
108+ query : {
109+ hearingId : hearingId ,
110+ v : activeVideo + 1
111+ }
112+ } ,
113+ undefined ,
114+ { shallow : true }
115+ )
116+ } else {
117+ router . replace (
118+ {
119+ pathname : router . pathname ,
120+ query : {
121+ hearingId : hearingId
122+ }
123+ } ,
124+ undefined ,
125+ { shallow : true }
126+ )
127+ }
128+ } , [ activeVideo ] )
62129
63- const handleVideoLoad = ( ) => {
64- setVideoLoaded ( true )
65- }
130+ // Runs once
131+ useEffect ( ( ) => {
132+ if ( ! router . isReady || routerReady . current ) return
133+ routerReady . current = true
134+
135+ const query = router . query . v
136+ if ( typeof query !== "string" ) {
137+ previousActive . current = activeVideo
138+ return
139+ }
140+ const n = parseInt ( query , 10 )
141+ if ( ! isNaN ( n ) && n >= 1 && n <= videos . length ) {
142+ setActiveVideo ( n - 1 )
143+ previousActive . current = n - 1
144+ }
145+ } , [ router . isReady ] )
146+
147+ useEffect ( ( ) => {
148+ ; ( async function ( ) {
149+ const transcripts = await Promise . all (
150+ videos . map ( v =>
151+ v . transcriptionId ? fetchTranscriptionData ( v . transcriptionId ) : null
152+ )
153+ )
154+ const result = transcripts . map ( ( t , index ) => {
155+ if ( ! t ) return null
156+ const filename =
157+ transcripts . length == 1
158+ ? `hearing-${ hearingId } `
159+ : `hearing-${ hearingId } -${ index + 1 } `
160+ const vtt = toVTT ( t )
161+ const blob = new Blob ( [ vtt ] , { type : "text/vtt" } )
162+
163+ return {
164+ title : videos [ index ] . title ,
165+ transcript : t ,
166+ blob : blob ,
167+ filename : filename
168+ }
169+ } )
170+ setTranscripts ( result )
171+ } ) ( )
172+ } , [ videos ] )
66173
67174 const videoRef = useRef < HTMLVideoElement > ( null )
68175 function setCurTimeVideo ( value : number ) {
@@ -78,14 +185,6 @@ export const HearingDetails = ({
78185 }
79186 } , [ router . query . t , videoRef . current ] )
80187
81- useEffect ( ( ) => {
82- ; ( async function ( ) {
83- if ( ! videoTranscriptionId || transcriptData !== null ) return
84- const docList = await fetchTranscriptionData ( videoTranscriptionId )
85- setTranscriptData ( docList )
86- } ) ( )
87- } , [ videoTranscriptionId ] )
88-
89188 return (
90189 < Container className = "mt-3 mb-3" >
91190 < Row className = { `mb-3` } >
@@ -94,7 +193,7 @@ export const HearingDetails = ({
94193 </ Col >
95194 </ Row >
96195
97- { transcriptData ? (
196+ { videos . length ? (
98197 < ButtonContainer className = { `mb-2` } >
99198 { /* ButtonContainer contrains clickable area of link so that it doesn't exceed
100199 the button and strech invisibly across the width of the page */ }
@@ -128,7 +227,7 @@ export const HearingDetails = ({
128227
129228 < Row >
130229 < Col className = { `col-md-8 mt-4` } >
131- { transcriptData ? (
230+ { transcripts !== null && transcripts . length > 0 ? (
132231 < LegalContainer className = { `pb-2 rounded` } >
133232 < Row
134233 className = { `d-flex align-items-center justify-content-between` }
@@ -164,47 +263,63 @@ export const HearingDetails = ({
164263 < > </ >
165264 ) }
166265
167- { videoURL ? (
168- < VideoParent className = { `mt-3` } >
169- < VideoChild
170- ref = { videoRef }
171- src = { videoURL }
172- onLoadedData = { handleVideoLoad }
173- controls
174- muted
175- />
176- </ VideoParent >
266+ { videos . length > 1 ? (
267+ < ButtonGroup aria-label = "Video buttons" className = { `mt-3` } >
268+ { videos . map ( ( video , index ) => (
269+ < VideoButton
270+ key = { index }
271+ variant = "link"
272+ $active = { activeVideo === index }
273+ onClick = { ( ) => setActiveVideo ( index ) }
274+ >
275+ { video . title }
276+ </ VideoButton >
277+ ) ) }
278+ </ ButtonGroup >
279+ ) : (
280+ < div className = { `mt-3` } > </ div >
281+ ) }
282+
283+ { videos . length > 0 ? (
284+ < >
285+ < VideoParent >
286+ < VideoChild
287+ ref = { videoRef }
288+ src = { videos [ activeVideo ] . url }
289+ controls
290+ muted
291+ />
292+ </ VideoParent >
293+ </ >
177294 ) : (
178295 < LegalContainer className = { `fs-6 fw-bold my-3 py-2 rounded` } >
179- { transcriptData
180- ? t ( "no_video_on_file" , { ns : "hearing" } )
181- : t ( "no_video_or_transcript" , { ns : "hearing" } ) }
296+ { t ( "no_video_or_transcript" , { ns : "hearing" } ) }
182297 </ LegalContainer >
183298 ) }
184299
185- { transcriptData ? (
300+ { transcripts && transcripts . length > 0 ? (
186301 < Transcriptions
302+ activeVideo = { activeVideo }
187303 hearingId = { hearingId }
188- transcriptData = { transcriptData }
304+ transcripts = { transcripts }
189305 setCurTimeVideo = { setCurTimeVideo }
190- videoLoaded = { videoLoaded }
191306 videoRef = { videoRef }
192307 />
193- ) : videoURL ? (
308+ ) : videos . length > 0 ? (
194309 < LegalContainer className = { `fs-6 fw-bold mb-2 py-2 rounded-bottom` } >
195- < div > { t ( "no_transcript_on_file " , { ns : "hearing" } ) } </ div >
310+ < div > { t ( "transcript_loading " , { ns : "hearing" } ) } </ div >
196311 </ LegalContainer >
197312 ) : null }
198313 </ Col >
199314
200315 < div className = { `col-md-4` } >
201316 < HearingSidebar
317+ activeVideo = { activeVideo }
202318 billsInAgenda = { billsInAgenda }
203319 committeeCode = { committeeCode }
204320 generalCourtNumber = { generalCourtNumber }
205321 hearingDate = { hearingDate }
206- hearingId = { hearingId }
207- transcriptData = { transcriptData }
322+ transcripts = { transcripts }
208323 />
209324 </ div >
210325 </ Row >
0 commit comments