diff --git a/components/hearing/HearingDetails.tsx b/components/hearing/HearingDetails.tsx index b8d3ce802..f79bdde5c 100644 --- a/components/hearing/HearingDetails.tsx +++ b/components/hearing/HearingDetails.tsx @@ -12,7 +12,7 @@ import { FeatureCalloutButton } from "../shared/CommonComponents" import { HearingSidebar } from "./HearingSidebar" -import { Paragraph, fetchTranscriptionData } from "./transcription" +import { HearingData, Paragraph, fetchTranscriptionData } from "./hearing" import { Transcriptions } from "./Transcriptions" const LegalContainer = styled(Container)` @@ -36,12 +36,22 @@ const VideoParent = styled.div` ` export const HearingDetails = ({ - hearingId + hearingData: { + billsInAgenda, + committeeCode, + committeeName, + description, + generalCourtNumber, + hearingDate, + hearingId, + videoTranscriptionId, + videoURL + } }: { - hearingId: string | string[] | undefined + hearingData: HearingData }) => { const { t } = useTranslation(["common", "hearing"]) - const [transcriptData, setTranscriptData] = useState([]) + const [transcriptData, setTranscriptData] = useState(null) const [videoLoaded, setVideoLoaded] = useState(false) const handleVideoLoad = () => { @@ -53,43 +63,14 @@ export const HearingDetails = ({ videoRef.current ? (videoRef.current.currentTime = value) : null } - const eventId = `hearing-${hearingId}` - - const [billsInAgenda, setBillsInAgenda] = useState([]) - const [committeeCode, setCommitteeCode] = useState("") - const [committeeName, setCommitteeName] = useState("") - const [description, setDescription] = useState("") - const [generalCourtNumber, setGeneralCourtNumber] = useState("") - const [hearingDate, setHearingDate] = useState("") - const [videoTranscriptionId, setVideoTranscriptionId] = useState("") - const [videoURL, setVideoURL] = useState("") - - const hearingData = useCallback(async () => { - const hearing = await getDoc(doc(firestore, `events/${eventId}`)) - const docData = hearing.data() - - setBillsInAgenda(docData?.content.HearingAgendas[0]?.DocumentsInAgenda) - setCommitteeCode(docData?.content.HearingHost.CommitteeCode) - setCommitteeName(docData?.content.Name) - setDescription(docData?.content.Description) - setGeneralCourtNumber(docData?.content.HearingHost.GeneralCourtNumber) - setHearingDate(docData?.content.EventDate) - setVideoTranscriptionId(docData?.videoTranscriptionId) - setVideoURL(docData?.videoURL) - }, [eventId]) - useEffect(() => { ;(async function () { - if (!videoTranscriptionId || transcriptData.length !== 0) return + if (!videoTranscriptionId || transcriptData !== null) return const docList = await fetchTranscriptionData(videoTranscriptionId) setTranscriptData(docList) })() }, [videoTranscriptionId]) - useEffect(() => { - hearingData() - }, [hearingData]) - return ( @@ -115,11 +96,15 @@ export const HearingDetails = ({ )} {committeeName ? ( -

- - {committeeName} - -

+ committeeCode ? ( +

+ + {committeeName} + +

+ ) : ( +

{committeeName}

+ ) ) : ( <> )} @@ -128,37 +113,41 @@ export const HearingDetails = ({ - - - -
- {t("bill.smart_tag")} + + +
+ {t("bill.smart_tag")} + {t("bill.smart_disclaimer2")} +
+ + + + + ]} /> - {t("bill.smart_disclaimer2")} -
- - - - - ]} - /> - -
-
+ +
+ + ) : ( + <> + )} {videoURL ? ( @@ -172,16 +161,24 @@ export const HearingDetails = ({ ) : ( - {t("no_video_on_file", { ns: "hearing" })} + {transcriptData + ? t("no_video_on_file", { ns: "hearing" }) + : t("no_video_or_transcript", { ns: "hearing" })} )} - + {transcriptData ? ( + + ) : videoURL ? ( + +
{t("no_transcript_on_file", { ns: "hearing" })}
+
+ ) : null}
diff --git a/components/hearing/HearingSidebar.tsx b/components/hearing/HearingSidebar.tsx index ff5c7c38d..a99e186be 100644 --- a/components/hearing/HearingSidebar.tsx +++ b/components/hearing/HearingSidebar.tsx @@ -10,7 +10,7 @@ import { firestore } from "../firebase" import * as links from "../links" import { billSiteURL, Internal } from "../links" import { LabeledIcon } from "../shared" -import { Paragraph, formatMilliseconds } from "./transcription" +import { Paragraph, formatMilliseconds } from "./hearing" type Bill = { BillNumber: string @@ -49,7 +49,7 @@ function MemberItem({ }, []) useEffect(() => { - generalCourtNumber ? memberData() : null + memberData() }, []) return ( @@ -122,23 +122,25 @@ export const HearingSidebar = ({ hearingId, transcriptData }: { - billsInAgenda: never[] - committeeCode: string - generalCourtNumber: string - hearingDate: string - hearingId: undefined | string | string[] - transcriptData: Paragraph[] + billsInAgenda: any[] | null + committeeCode: string | null + generalCourtNumber: string | null + hearingDate: string | null + hearingId: string + transcriptData: Paragraph[] | null }) => { const { t } = useTranslation(["common", "hearing"]) - const dateObject = new Date(hearingDate) - const formattedDate = dateObject.toLocaleDateString("en-US", { - month: "long", - day: "2-digit", - year: "numeric" - }) + const dateObject = hearingDate ? new Date(hearingDate) : null + const formattedDate = dateObject + ? dateObject.toLocaleDateString("en-US", { + month: "long", + day: "2-digit", + year: "numeric" + }) + : null let dateCheck = false - if (formattedDate !== `Invalid Date`) { + if (formattedDate && formattedDate !== `Invalid Date`) { dateCheck = true } @@ -185,15 +187,11 @@ export const HearingSidebar = ({ }, [committeeCode, generalCourtNumber]) useEffect(() => { - if (!hearingId) { - setDownloadName("hearing.csv") - } else { - setDownloadName(`hearing-${hearingId}.csv`) - } + setDownloadName(`hearing-${hearingId}.csv`) }, [hearingId]) useEffect(() => { - if (transcriptData.length === 0) return + if (!transcriptData) return const csv_objects = transcriptData.map(doc => ({ start: formatMilliseconds(doc.start), text: doc.text @@ -225,7 +223,7 @@ export const HearingSidebar = ({ {t("hearing_details", { ns: "hearing" })} - {dateCheck || (downloadURL !== "" && hearingId !== undefined) ? ( + {dateCheck || downloadURL !== "" ? ( {dateCheck ? ( <> @@ -237,7 +235,7 @@ export const HearingSidebar = ({ ) : ( <> )} - {downloadURL !== "" && hearingId !== undefined ? ( + {downloadURL !== "" ? ( - {committeeActions[0]?.Votes[0]?.Vote[0]?.Favorable.map( - (element: any, index: number) => ( - - ) - )} + {generalCourtNumber && + committeeActions[0]?.Votes[0]?.Vote[0]?.Favorable.map( + (element: any, index: number) => ( + + ) + )}
{t("no", { ns: "hearing" })} ( {committeeActions[0]?.Votes[0]?.Vote[0]?.Adverse.length})
- {committeeActions[0]?.Votes[0]?.Vote[0]?.Adverse.map( - (element: any, index: number) => ( - - ) - )} + {generalCourtNumber && + committeeActions[0]?.Votes[0]?.Vote[0]?.Adverse.map( + (element: any, index: number) => ( + + ) + )}
{t("no_vote", { ns: "hearing" })} ( {committeeActions[0]?.Votes[0]?.Vote[0]?.NoVoteRecorded.length})
- {committeeActions[0]?.Votes[0]?.Vote[0]?.NoVoteRecorded.map( - (element: any, index: number) => ( - - ) - )} + {generalCourtNumber && + committeeActions[0]?.Votes[0]?.Vote[0]?.NoVoteRecorded.map( + (element: any, index: number) => ( + + ) + )}
{t("reserve_right", { ns: "hearing" })} ( {committeeActions[0]?.Votes[0]?.Vote[0]?.ReserveRight.length})
- {committeeActions[0]?.Votes[0]?.Vote[0]?.ReserveRight.map( - (element: any, index: number) => ( - - ) - )} + {generalCourtNumber && + committeeActions[0]?.Votes[0]?.Vote[0]?.ReserveRight.map( + (element: any, index: number) => ( + + ) + )}
@@ -588,7 +590,7 @@ function Vote({ }, []) useEffect(() => { - generalCourtNumber ? memberData() : null + memberData() }, []) return ( diff --git a/components/hearing/Transcriptions.tsx b/components/hearing/Transcriptions.tsx index caf468bd0..7d2d5284a 100644 --- a/components/hearing/Transcriptions.tsx +++ b/components/hearing/Transcriptions.tsx @@ -4,7 +4,7 @@ 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 } from "./transcription" +import { Paragraph, formatMilliseconds } from "./hearing" const ClearButton = styled(FontAwesomeIcon)` position: absolute; @@ -26,10 +26,6 @@ const ResultNumText = styled.div` color: #979797; ` -const ErrorContainer = styled(Container)` - background-color: white; -` - const NoResultFound = styled.div` display: flex; justify-content: center; @@ -216,43 +212,35 @@ export const Transcriptions = ({ )} - {transcriptData.length > 0 ? ( - <> - - {filteredData.map((element: Paragraph, index: number) => ( - { - if (elem) { - transcriptRefs.current.set(index, elem) - } else { - transcriptRefs.current.delete(index) - } - }} - setCurTimeVideo={setCurTimeVideo} - searchTerm={searchTerm} - /> - ))} - {filteredData.length === 0 && ( - - {t("no_results_found", { - ns: "hearing", - searchTerm, - defaultValue: "No result found..." - })} - - )} - - - - ) : ( - -
{t("transcription_not_on_file", { ns: "hearing" })}
-
- )} + + {filteredData.map((element: Paragraph, index: number) => ( + { + if (elem) { + transcriptRefs.current.set(index, elem) + } else { + transcriptRefs.current.delete(index) + } + }} + setCurTimeVideo={setCurTimeVideo} + searchTerm={searchTerm} + /> + ))} + {filteredData.length === 0 && ( + + {t("no_results_found", { + ns: "hearing", + searchTerm, + defaultValue: "No result found..." + })} + + )} + + ) } diff --git a/components/hearing/hearing.ts b/components/hearing/hearing.ts new file mode 100644 index 000000000..da809c6ee --- /dev/null +++ b/components/hearing/hearing.ts @@ -0,0 +1,99 @@ +import { firestore } from "../firebase" +import { + collection, + doc, + getDoc, + getDocs, + orderBy, + query +} from "firebase/firestore" +import { DateTime } from "luxon" + +export type HearingData = { + billsInAgenda: any[] | null + committeeCode: string | null + committeeName: string | null + description: string | null + generalCourtNumber: string | null + // Date and DateTime cannot be sent through getServerSideProps + hearingDate: string | null + hearingId: string + videoTranscriptionId: string | null + videoURL: string | null +} + +export type Paragraph = { + confidence: number + end: number + start: number + text: string +} + +export async function fetchHearingData( + hearingId: string +): Promise { + const hearing = await getDoc(doc(firestore, `events/hearing-${hearingId}`)) + if (!hearing.exists()) { + return null + } + + const docData = hearing.data() + + const maybeDate = docData.content?.EventDate + // Event has no provided timezone + const hearingDate = maybeDate + ? DateTime.fromISO(maybeDate, { zone: "America/New_York" }).toISO() + : null + + return { + billsInAgenda: + docData.content?.HearingAgendas[0]?.DocumentsInAgenda ?? null, + committeeCode: docData.content?.HearingHost?.CommitteeCode ?? null, + committeeName: docData.content?.Name ?? null, + description: docData.content?.Description ?? null, + generalCourtNumber: + docData.content?.HearingHost?.GeneralCourtNumber ?? null, + hearingDate: hearingDate, + hearingId: hearingId, + videoTranscriptionId: docData.videoTranscriptionId ?? null, + videoURL: docData.videoURL ?? null + } +} + +export async function fetchTranscriptionData( + videoTranscriptionId: string +): Promise { + const subscriptionRef = collection( + firestore, + `transcriptions/${videoTranscriptionId}/paragraphs` + ) + + let docList: any[] = [] + + const q = query(subscriptionRef, orderBy("start")) + const querySnapshot = await getDocs(q) + + querySnapshot.forEach(doc => { + // doc.data() is never undefined for query doc snapshots + docList.push(doc.data()) + }) + + return docList +} + +export function formatMilliseconds(ms: number): string { + const totalSeconds = Math.floor(ms / 1000) + const hours = Math.floor(totalSeconds / 3600) + const minutes = Math.floor((totalSeconds % 3600) / 60) + const seconds = totalSeconds % 60 + + const formattedHours = String(hours).padStart(2, "0") + const formattedMinutes = String(minutes).padStart(2, "0") + const formattedSeconds = String(seconds).padStart(2, "0") + + if (hours >= 1) { + return `${formattedHours}:${formattedMinutes}:${formattedSeconds}` + } else { + return `${formattedMinutes}:${formattedSeconds}` + } +} diff --git a/components/hearing/transcription.ts b/components/hearing/transcription.ts deleted file mode 100644 index d9069bd9d..000000000 --- a/components/hearing/transcription.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { firestore } from "../firebase" -import { collection, getDocs, orderBy, query } from "firebase/firestore" - -export type Paragraph = { - confidence: number - end: number - start: number - text: string -} - -export async function fetchTranscriptionData( - videoTranscriptionId: string -): Promise { - const subscriptionRef = collection( - firestore, - `transcriptions/${videoTranscriptionId}/paragraphs` - ) - - let docList: any[] = [] - - const q = query(subscriptionRef, orderBy("start")) - const querySnapshot = await getDocs(q) - - querySnapshot.forEach(doc => { - // doc.data() is never undefined for query doc snapshots - docList.push(doc.data()) - }) - - return docList -} - -export function formatMilliseconds(ms: number): string { - const totalSeconds = Math.floor(ms / 1000) - const hours = Math.floor(totalSeconds / 3600) - const minutes = Math.floor((totalSeconds % 3600) / 60) - const seconds = totalSeconds % 60 - - const formattedHours = String(hours).padStart(2, "0") - const formattedMinutes = String(minutes).padStart(2, "0") - const formattedSeconds = String(seconds).padStart(2, "0") - - if (hours >= 1) { - return `${formattedHours}:${formattedMinutes}:${formattedSeconds}` - } else { - return `${formattedMinutes}:${formattedSeconds}` - } -} diff --git a/pages/hearing/[hearingId].tsx b/pages/hearing/[hearingId].tsx index 99d355b9e..73a0ad6b7 100644 --- a/pages/hearing/[hearingId].tsx +++ b/pages/hearing/[hearingId].tsx @@ -5,14 +5,14 @@ import { z } from "zod" import { flags } from "components/featureFlags" import { HearingDetails } from "components/hearing/HearingDetails" import { createPage } from "../../components/page" +import { fetchHearingData, HearingData } from "components/hearing/hearing" const Query = z.object({ hearingId: z.coerce.number() }) -export default createPage<{ hearingId: number }>({ +export default createPage<{ hearingData: HearingData }>({ titleI18nKey: "Hearing", - Page: () => { - const hearingId = useRouter().query.hearingId - return + Page: ({ hearingData }) => { + return } }) @@ -28,8 +28,16 @@ export const getServerSideProps: GetServerSideProps = async ctx => { if (!query.success) return { notFound: true } if (!flags().hearingsAndTranscriptions) return { notFound: true } + if (!ctx.params || !ctx.params.hearingId) return { notFound: true } + const hearingId = Array.isArray(ctx.params.hearingId) + ? ctx.params.hearingId.join("-") + : ctx.params.hearingId + const hearingData = await fetchHearingData(hearingId) + if (!hearingData) return { notFound: true } + return { props: { + hearingData: hearingData, ...(await serverSideTranslations(locale, [ "auth", "common", diff --git a/public/locales/en/hearing.json b/public/locales/en/hearing.json index e0da2c35a..b16347b7f 100644 --- a/public/locales/en/hearing.json +++ b/public/locales/en/hearing.json @@ -11,7 +11,9 @@ "no": "No", "no_record": "No Record", "no_results_found": "No Search Results for ”{{searchTerm}}”", + "no_transcript_on_file": "This hearing does not yet have a transcription on file", "no_video_on_file": "This hearing does not yet have a video on file", + "no_video_or_transcript": "This hearing does not yet have a video or transcript on file", "no_vote": "No Vote Recorded", "num_results_one": "{{count}} result", "num_results_other": "{{count}} results", @@ -24,7 +26,6 @@ "see_all": "See all", "see_less": "See less", "senate_chair": "Senate Chair", - "transcription_not_on_file": "This hearing does not yet have a transcription on file", "video_and_transcription_feature_callout": "Hearing Video + Transcription", "view_bill": "View Bill Details", "view_votes": "View Committee Votes",