Skip to content

Commit 710c5db

Browse files
committed
Adding hearing-specific page - will not work until we actually start transcribing hearings
1 parent 80c3626 commit 710c5db

File tree

1 file changed

+150
-0
lines changed

1 file changed

+150
-0
lines changed

pages/hearing/[...hearingId].tsx

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { useEffect, useRef, useState } from "react"
2+
import Head from "next/head"
3+
import styles from "../../styles/VideoTranscription.module.css" // Adjust the path as necessary
4+
import { firestore } from "../../components/firebase"
5+
import { doc, getDoc } from "firebase/firestore"
6+
import { z } from "zod"
7+
import { GetServerSideProps } from "next"
8+
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
9+
10+
const Query = z.object({ hearingId: z.string({}) })
11+
12+
export default function VideoTranscription({
13+
videoUrl,
14+
utterances
15+
}: {
16+
videoUrl: any
17+
utterances: Array<any>
18+
}) {
19+
const [currentTime, setCurrentTime] = useState(0)
20+
const videoRef = useRef<any>(null)
21+
const transcriptionRef = useRef<any>(null)
22+
const utteranceRefs = useRef<any>({})
23+
24+
// Update current time when video plays
25+
const handleTimeUpdate = () => {
26+
if (videoRef.current) {
27+
setCurrentTime(videoRef.current.currentTime * 1000) // Convert to ms
28+
}
29+
}
30+
31+
// Scroll to the current utterance
32+
useEffect(() => {
33+
const currentUtterance = utterances.find(
34+
utterance =>
35+
currentTime >= utterance.start && currentTime <= utterance.end
36+
)
37+
38+
if (currentUtterance && utteranceRefs.current[currentUtterance.start]) {
39+
const element = utteranceRefs.current[currentUtterance.start]
40+
const container = transcriptionRef.current
41+
42+
if (container) {
43+
container.scrollTop = element.offsetTop - container.offsetTop - 100 // Offset for better visibility
44+
}
45+
}
46+
}, [currentTime, utterances])
47+
48+
// Click on transcription to seek video
49+
const seekToTime = (startTime: number) => {
50+
if (videoRef.current) {
51+
videoRef.current.currentTime = startTime / 1000 // Convert ms to seconds
52+
}
53+
}
54+
55+
return (
56+
<div className={styles.container}>
57+
<Head>
58+
<title>Video Transcription</title>
59+
<meta
60+
name="description"
61+
content="Video with synchronized transcription"
62+
/>
63+
</Head>
64+
65+
<main className={styles.main}>
66+
<div className={styles.videoContainer}>
67+
<video
68+
ref={videoRef}
69+
src={videoUrl}
70+
controls
71+
onTimeUpdate={handleTimeUpdate}
72+
className={styles.video}
73+
/>
74+
</div>
75+
76+
<div className={styles.transcriptionContainer} ref={transcriptionRef}>
77+
<h2>Transcription</h2>
78+
<div className={styles.transcription}>
79+
{utterances.map(utterance => {
80+
const isActive =
81+
currentTime >= utterance.start && currentTime <= utterance.end
82+
return (
83+
<div
84+
key={utterance.start}
85+
ref={el => (utteranceRefs.current[utterance.start] = el)}
86+
className={`${styles.utterance} ${
87+
isActive ? styles.active : ""
88+
}`}
89+
onClick={() => seekToTime(utterance.start)}
90+
>
91+
<span className={styles.timestamp}>
92+
{formatTime(utterance.start)} - {formatTime(utterance.end)}
93+
</span>
94+
<p>{utterance.text}</p>
95+
</div>
96+
)
97+
})}
98+
</div>
99+
</div>
100+
</main>
101+
</div>
102+
)
103+
}
104+
105+
// Helper function to format milliseconds to MM:SS format
106+
function formatTime(ms: number) {
107+
const totalSeconds = Math.floor(ms / 1000)
108+
const minutes = Math.floor(totalSeconds / 60)
109+
const seconds = totalSeconds % 60
110+
return `${minutes.toString().padStart(2, "0")}:${seconds
111+
.toString()
112+
.padStart(2, "0")}`
113+
}
114+
115+
export const getServerSideProps: GetServerSideProps = async ctx => {
116+
const locale = ctx.locale ?? ctx.defaultLocale ?? "en"
117+
const query = Query.safeParse(ctx.query)
118+
if (!query.success) return { notFound: true }
119+
const { hearingId } = query.data
120+
121+
const rawHearing = await getDoc(doc(firestore, `events/hearing-${hearingId}`))
122+
if (!rawHearing.exists) return { notFound: true }
123+
124+
const hearing = rawHearing.data() as any
125+
const transcriptionId = hearing.videoAssemblyId
126+
if (!transcriptionId) return { notFound: true }
127+
128+
const rawTranscription = await getDoc(
129+
doc(firestore, `transcriptions/${transcriptionId}`)
130+
)
131+
if (!rawTranscription.exists()) return { notFound: true }
132+
const transcription = rawTranscription.data() as any
133+
134+
const videoUrl = transcription.data().audio_url
135+
const utterances = transcription.data().utterances
136+
137+
return {
138+
props: {
139+
videoUrl,
140+
utterances,
141+
...(await serverSideTranslations(locale, [
142+
"auth",
143+
"common",
144+
"footer",
145+
"testimony",
146+
"profile"
147+
]))
148+
}
149+
}
150+
}

0 commit comments

Comments
 (0)