Skip to content

Commit d98574b

Browse files
authored
Bill to Hearing Links (#2000)
* lay down basic hearing-link layout * breakpoints * conflict resolution 2 * add hearingIds to bill type * add translations * modal foundation * modal title * hearing details * fixed internal links * translations * translation * refactor hearingIds * refactor hearingIds * resolve dupliucate hearingIds
1 parent 13a748c commit d98574b

9 files changed

Lines changed: 42370 additions & 2754 deletions

File tree

components/bill/SmartDisclaimer.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import Image from "react-bootstrap/Image"
33
import styled from "styled-components"
44
import { useMediaQuery } from "usehooks-ts"
55
import { Col, Row } from "../bootstrap"
6-
import { NavLink } from "components/Navlink"
76
import * as links from "../links"
87

98
const SmartTagDesc = styled(Row)`

components/bill/Summary.tsx

Lines changed: 265 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,23 @@
1-
import { useFlags } from "components/featureFlags"
2-
import * as links from "components/links"
1+
import { doc, getDoc } from "firebase/firestore"
32
import { useTranslation } from "next-i18next"
4-
import { useState } from "react"
5-
import styled from "styled-components"
6-
import { Button, Col, Container, Modal, Row } from "../bootstrap"
3+
import { useCallback, useEffect, useState } from "react"
4+
import type { ModalProps } from "react-bootstrap"
5+
import styled, { ThemeConsumer } from "styled-components"
6+
import { useMediaQuery } from "usehooks-ts"
7+
import { Button, Col, Container, Image, Modal, Row, Stack } from "../bootstrap"
8+
import { useFlags } from "../featureFlags"
9+
import { firestore } from "../firebase"
10+
import * as links from "../links"
11+
import {
12+
ButtonContainer,
13+
FeatureCalloutButton
14+
} from "../shared/CommonComponents"
715
import { SmartDisclaimer } from "./SmartDisclaimer"
816
import { SmartIcon } from "./SmartIcon"
917
import { TestimonyCounts } from "./TestimonyCounts"
1018
import { BillProps } from "./types"
11-
import { currentGeneralCourt } from "functions/src/shared"
1219
import { BillTopic } from "functions/src/bills/types"
13-
14-
const SummaryContainer = styled(Container)`
15-
background-color: white;
16-
border-radius: 0.75rem;
17-
padding: 1rem;
18-
background-image: url("/quote-left.svg");
19-
background-repeat: no-repeat;
20-
background-size: 4rem;
21-
background-position: 0.5rem 0.5rem;
22-
`
23-
24-
const TitleFormat = styled(Col)`
25-
margin-top: 1rem;
26-
font-style: italic;
27-
font-size: 1.25rem;
28-
`
20+
import { currentGeneralCourt } from "functions/src/shared"
2921

3022
const Divider = styled(Col)`
3123
width: 2px;
@@ -34,23 +26,10 @@ const Divider = styled(Col)`
3426
align-self: stretch;
3527
`
3628

37-
const StyledButton = styled(Button)`
38-
:focus {
39-
box-shadow: none;
40-
}
41-
padding: 0;
42-
margin: 0;
43-
`
44-
4529
const FormattedBillDetails = styled(Col)`
4630
white-space: pre-wrap;
4731
`
4832

49-
const SmartTagButton = styled.button`
50-
border-radius: 12px;
51-
font-size: 12px;
52-
`
53-
5433
const SmartTag = ({ topic }: { topic: BillTopic }) => {
5534
return (
5635
<links.Internal
@@ -69,6 +48,55 @@ const SmartTag = ({ topic }: { topic: BillTopic }) => {
6948
)
7049
}
7150

51+
const SmartTagButton = styled.button`
52+
border-radius: 12px;
53+
font-size: 12px;
54+
`
55+
56+
const StyledButton = styled(Button)`
57+
:focus {
58+
box-shadow: none;
59+
}
60+
padding: 0;
61+
margin: 0;
62+
`
63+
64+
const SummaryContainer = styled(Container)`
65+
background-color: white;
66+
border-radius: 0.75rem;
67+
padding: 1rem;
68+
background-image: url("/quote-left.svg");
69+
background-repeat: no-repeat;
70+
background-size: 4rem;
71+
background-position: 0.5rem 0.5rem;
72+
`
73+
74+
const TitleFormat = styled(Col)`
75+
margin-top: 1rem;
76+
font-style: italic;
77+
font-size: 1.25rem;
78+
`
79+
80+
export const ViewButton = styled.button`
81+
border-radius: 4px;
82+
border-width: 2px;
83+
height: fit-content;
84+
margin-top: 16px;
85+
width: fit-content;
86+
87+
@media (max-width: 425px) {
88+
margin-top: 32px;
89+
}
90+
`
91+
92+
export const ViewMessage = styled.div`
93+
width: 300px;
94+
95+
@media (max-width: 425px) {
96+
width: auto;
97+
}
98+
`
99+
72100
export const Summary = ({
73101
bill,
74102
className
@@ -77,10 +105,12 @@ export const Summary = ({
77105
const handleShowBillDetails = () => setShowBillDetails(true)
78106
const handleHideBillDetails = () => setShowBillDetails(false)
79107
const billText = bill?.content?.DocumentText
108+
const hearingIds = bill?.hearingIds
80109

81110
const { showLLMFeatures } = useFlags()
82111

83112
const { t } = useTranslation("common")
113+
const isMobile = useMediaQuery("(max-width: 991px)")
84114

85115
return (
86116
<SummaryContainer className={className}>
@@ -126,6 +156,45 @@ export const Summary = ({
126156
<Col className={`d-flex`} xs="auto">
127157
<TestimonyCounts bill={bill} />
128158
</Col>
159+
160+
{hearingIds ? (
161+
<>
162+
{isMobile ? (
163+
<div>
164+
<hr className={`m-0 border-bottom border-2`} />
165+
</div>
166+
) : (
167+
<Divider className={`my-2`} xs="auto" />
168+
)}
169+
170+
<Col className={`d-flex my-3`} xs="auto">
171+
<Stack>
172+
<Row className={`d-flex flex-nowrap`}>
173+
<ButtonContainer className={`mb-2 pe-1`}>
174+
<FeatureCalloutButton
175+
className={`btn btn-secondary d-flex text-nowrap mt-1 mx-1 p-0`}
176+
>
177+
&nbsp; {t("new_feature")} &nbsp;
178+
</FeatureCalloutButton>
179+
</ButtonContainer>
180+
<div className={`d-flex justify-content-start ps-0`}>
181+
{t("bill.hearing_video_and_transcript", { ns: "common" })}
182+
</div>
183+
</Row>
184+
<ViewMessage>
185+
{t("view_the_hearing", { ns: "common" })}
186+
</ViewMessage>
187+
</Stack>
188+
<Stack className={`ms-3`}>
189+
<ButtonContainer className={`d-flex align-self-center`}>
190+
<ViewChild bill={bill} />
191+
</ButtonContainer>
192+
</Stack>
193+
</Col>
194+
</>
195+
) : (
196+
<></>
197+
)}
129198
</Row>
130199
{showLLMFeatures ? (
131200
<>
@@ -150,3 +219,164 @@ export const Summary = ({
150219
</SummaryContainer>
151220
)
152221
}
222+
223+
const ViewChild = ({ bill }: BillProps) => {
224+
const { t } = useTranslation("common")
225+
226+
const hearingIds = bill.hearingIds
227+
228+
const [hearingNums, setHearingNums] = useState(bill.hearingIds)
229+
const [hearingsModal, setHearingsModal] = useState<"show" | null>(null)
230+
231+
const close = () => setHearingsModal(null)
232+
233+
const substringToRemove = "hearing-"
234+
const removeSubstring = () => {
235+
const editedHearingIds = hearingIds?.map(str =>
236+
str.replace(new RegExp(substringToRemove, "g"), "")
237+
)
238+
setHearingNums(editedHearingIds)
239+
}
240+
241+
useEffect(() => {
242+
hearingIds ? removeSubstring() : null
243+
}, [])
244+
245+
return (
246+
<>
247+
{hearingIds?.length === 1 ? (
248+
<links.Internal href={`/hearing/${hearingNums}`} className="">
249+
<ViewButton className={`btn btn-outline-secondary fw-bold p-1`}>
250+
{t("view", { ns: "common" })}
251+
</ViewButton>
252+
</links.Internal>
253+
) : (
254+
<ViewButton
255+
className={`btn btn-outline-secondary fw-bold p-1`}
256+
onClick={() => setHearingsModal("show")}
257+
>
258+
{t("view", { ns: "common" })}
259+
</ViewButton>
260+
)}
261+
<HearingsModal
262+
hearingNums={hearingNums}
263+
onHide={close}
264+
onHearingsModalClose={() => setHearingsModal(null)}
265+
show={hearingsModal === "show"}
266+
/>
267+
</>
268+
)
269+
}
270+
271+
type Props = Pick<ModalProps, "show" | "onHide"> & {
272+
hearingNums: string[] | undefined
273+
onHearingsModalClose: () => void
274+
}
275+
276+
function HearingsModal({
277+
hearingNums,
278+
onHide,
279+
onHearingsModalClose,
280+
show
281+
}: Props) {
282+
const { t } = useTranslation(["common"])
283+
284+
return (
285+
<Modal
286+
show={show}
287+
onHide={onHide}
288+
aria-labelledby="hearings-modal"
289+
centered
290+
>
291+
<Modal.Header className={`px-3 py-1`}>
292+
<Modal.Title id="hearings-modal">
293+
<b>{t("bill.select_hearing", { ns: "common" })}</b>
294+
</Modal.Title>
295+
<Image
296+
src="/x_cancel.png"
297+
alt={t("navigation.closeNavMenu", { ns: "editProfile" })}
298+
width="25"
299+
height="25"
300+
className="ms-2"
301+
onClick={onHearingsModalClose}
302+
/>
303+
</Modal.Header>
304+
<Modal.Body className={`bg-white p-3`}>
305+
<hr className={`m-0 border-bottom border-2`} />
306+
<p className={`fw-bold fs-6`}>
307+
{t("bill.multiple_hearings", { ns: "common" })}
308+
</p>
309+
{hearingNums?.map((element: any, index: number) => (
310+
<Hearing key={index} hearingId={element} />
311+
))}
312+
</Modal.Body>
313+
</Modal>
314+
)
315+
}
316+
317+
const HearingRow = styled.div`
318+
&:nth-child(odd) {
319+
background-color: #eae7e7;
320+
&:nth-child(even) {
321+
background-color: white;
322+
}
323+
`
324+
325+
function Hearing({ hearingId }: { hearingId: string }) {
326+
const [committeeCode, setCommitteeCode] = useState("")
327+
const [committeeName, setCommitteeName] = useState("")
328+
const [eventDate, setEventDate] = useState("")
329+
const [generalCourtNumber, setGeneralCourtNumber] = useState("")
330+
331+
const hearingData = useCallback(async () => {
332+
const hearing = await getDoc(doc(firestore, `events/hearing-${hearingId}`))
333+
const docData = hearing.data()
334+
335+
setCommitteeCode(docData?.content.HearingHost.CommitteeCode)
336+
setCommitteeName(docData?.content.Name)
337+
setEventDate(docData?.content.EventDate)
338+
setGeneralCourtNumber(docData?.content.HearingHost.GeneralCourtNumber)
339+
}, [hearingId])
340+
341+
useEffect(() => {
342+
hearingId ? hearingData() : null
343+
}, [])
344+
345+
const [branch, setBranch] = useState("")
346+
347+
const committeeData = useCallback(async () => {
348+
const committee = await getDoc(
349+
doc(
350+
firestore,
351+
`generalCourts/${generalCourtNumber}/committees/${committeeCode}`
352+
)
353+
)
354+
const docData = committee.data()
355+
setBranch(docData?.content?.Branch)
356+
}, [committeeCode, generalCourtNumber])
357+
358+
useEffect(() => {
359+
committeeCode && generalCourtNumber ? committeeData() : null
360+
}, [committeeCode, committeeData, generalCourtNumber])
361+
362+
const date = new Date(eventDate)
363+
const month = (date.getMonth() + 1).toString().padStart(2, "0")
364+
const day = date.getDate().toString().padStart(2, "0")
365+
const year = date.getFullYear()
366+
367+
return (
368+
<HearingRow className={`d-flex justify-content-between w-auto`}>
369+
<Col xs={3} className={`mx-2`}>
370+
{`${month}-${day}-${year}`}
371+
</Col>
372+
<Col xs={7} className={`me-2`}>
373+
<links.Internal href={`/hearing/${hearingId}`}>
374+
{committeeName}
375+
</links.Internal>
376+
</Col>
377+
<Col xs={2} className={``}>
378+
{branch}
379+
</Col>
380+
</HearingRow>
381+
)
382+
}

components/db/bills.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ export type Bill = {
5151
opposeCount: number
5252
neutralCount: number
5353
nextHearingAt?: Timestamp
54-
hearingIds?: string[]
5554
latestTestimonyAt?: Timestamp
5655
latestTestimonyId?: string
5756
fetchedAt: Timestamp
@@ -60,6 +59,7 @@ export type Bill = {
6059
city?: string
6160
topics?: BillTopic[]
6261
summary?: string
62+
hearingIds?: string[]
6363
}
6464

6565
export function useBill(court: number, id: string) {

0 commit comments

Comments
 (0)