Skip to content

Commit 974f35c

Browse files
committed
Added multiple video handling to frontend
1 parent 28baa9a commit 974f35c

5 files changed

Lines changed: 269 additions & 109 deletions

File tree

components/hearing/HearingDetails.tsx

Lines changed: 154 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { useRouter } from "next/router"
22
import { Trans, useTranslation } from "next-i18next"
33
import { useEffect, useRef, useState } from "react"
44
import 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"
67
import * as links from "../links"
78
import { committeeURL, External } from "../links"
89
import {
@@ -14,8 +15,10 @@ import { HearingSidebar } from "./HearingSidebar"
1415
import {
1516
HearingData,
1617
Paragraph,
18+
TranscriptData,
1719
convertToString,
18-
fetchTranscriptionData
20+
fetchTranscriptionData,
21+
toVTT
1922
} from "./hearing"
2023
import { 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+
4273
export 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>

components/hearing/HearingSidebar.tsx

Lines changed: 18 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { firestore } from "../firebase"
99
import * as links from "../links"
1010
import { billSiteURL, Internal } from "../links"
1111
import { LabeledIcon } from "../shared"
12-
import { Paragraph, formatVTTTimestamp } from "./hearing"
12+
import { Paragraph, TranscriptData, formatVTTTimestamp } from "./hearing"
1313

1414
type Bill = {
1515
BillNumber: string
@@ -114,19 +114,19 @@ const SidebarSubbody = styled.div`
114114
`
115115

116116
export const HearingSidebar = ({
117+
activeVideo,
117118
billsInAgenda,
118119
committeeCode,
119120
generalCourtNumber,
120121
hearingDate,
121-
hearingId,
122-
transcriptData
122+
transcripts
123123
}: {
124+
activeVideo: number
124125
billsInAgenda: any[] | null
125126
committeeCode: string | null
126127
generalCourtNumber: string | null
127128
hearingDate: string | null
128-
hearingId: string
129-
transcriptData: Paragraph[] | null
129+
transcripts: (TranscriptData | null)[] | null
130130
}) => {
131131
const { t } = useTranslation(["common", "hearing"])
132132

@@ -186,35 +186,14 @@ export const HearingSidebar = ({
186186
}, [committeeCode, generalCourtNumber])
187187

188188
useEffect(() => {
189-
setDownloadName(`hearing-${hearingId}.vtt`)
190-
}, [hearingId])
191-
192-
useEffect(() => {
193-
if (!transcriptData) return
194-
const vttLines = ["WEBVTT", ""]
195-
196-
transcriptData.forEach((paragraph, index) => {
197-
const cueNumber = index + 1
198-
const startTime = formatVTTTimestamp(paragraph.start)
199-
const endTime = formatVTTTimestamp(paragraph.end)
200-
201-
vttLines.push(
202-
String(cueNumber),
203-
`${startTime} --> ${endTime}`,
204-
paragraph.text,
205-
""
206-
)
207-
})
208-
209-
const vtt = vttLines.join("\n")
210-
const blob = new Blob([vtt], { type: "text/vtt" })
211-
const url = URL.createObjectURL(blob)
189+
if (!transcripts || !transcripts[activeVideo]) return
190+
setDownloadName(transcripts[activeVideo]!.filename)
191+
const url = URL.createObjectURL(transcripts[activeVideo]!.blob)
212192
setDownloadURL(url)
213-
214193
return () => {
215194
URL.revokeObjectURL(url)
216195
}
217-
}, [transcriptData])
196+
}, [activeVideo, transcripts])
218197

219198
useEffect(() => {
220199
committeeCode && generalCourtNumber ? committeeData() : null
@@ -245,14 +224,21 @@ export const HearingSidebar = ({
245224
) : (
246225
<></>
247226
)}
248-
{downloadURL !== "" ? (
227+
{downloadURL !== "" &&
228+
transcripts !== null &&
229+
transcripts[activeVideo] !== null ? (
249230
<div>
250231
<a
251232
href={downloadURL}
252233
download={downloadName}
253234
className="text-blue-600 underline"
254235
>
255-
{t("download_transcript", { ns: "hearing" })}
236+
{transcripts.length == 1
237+
? t("download_transcript", { ns: "hearing" })
238+
: t("download_transcript_x", {
239+
ns: "hearing",
240+
title: transcripts[activeVideo]!.title
241+
})}
256242
</a>
257243
</div>
258244
) : (

0 commit comments

Comments
 (0)