Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions components/AnnouncementBanner/AnnouncementBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Row } from "react-bootstrap"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faXmark } from "@fortawesome/free-solid-svg-icons"
import { useLocalStorage } from "usehooks-ts"

function AnnouncementBanner({
endDate,
children
}: {
endDate: Date
children: React.ReactElement
}) {
const [open, setOpen] = useLocalStorage<boolean>("bannerOpen", true)

const close = () => {
setOpen(false)
}

const now: Date = new Date()
if (now < endDate && open) {
return (
<Row
style={{ zIndex: 100 }}
className="position-relative h3 text-center text-white p-3 bg-warning"
>
<button
className="position-absolute top-0 end-0 border-0 bg-transparent p-1"
style={{ width: "auto" }}
onClick={close}
>
<FontAwesomeIcon
icon={faXmark}
style={{ color: "white", fontSize: "18px", paddingRight: "8px" }}
/>
</button>

{children}
</Row>
)
} else {
return <></>
}
}

export default AnnouncementBanner
23 changes: 23 additions & 0 deletions components/AnnouncementBanner/TranscriptAnnouncement.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next"
import AnnouncementBanner from "./AnnouncementBanner"

function TranscriptAnnouncement() {
const { t } = useTranslation("common")
return (
<AnnouncementBanner endDate={new Date("2026-03-01T12:00:00.000Z")}>
<p className="mb-0">
<span className="fw-bold">{t("announcement.headingBold")}</span>{" "}
<span>
{t("announcement.headingBody")}
<br />
{t("announcement.description")}{" "}
<a href="/hearings" style={{ color: "white" }}>
{t("announcement.link")}
</a>
</span>
</p>
</AnnouncementBanner>
)
}

export default TranscriptAnnouncement
2 changes: 2 additions & 0 deletions components/HeroHeader/HeroHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import styles from "./HeroHeader.module.css"
import { useTranslation } from "next-i18next"
import { capitalize } from "lodash"
import { NEWSLETTER_SIGNUP_URL, TRAINING_CALENDAR_URL } from "../common"
import TranscriptAnnouncement from "components/AnnouncementBanner/TranscriptAnnouncement"

const HeroHeader = ({ authenticated }) => {
const { t } = useTranslation("common")
return (
<Container fluid className={`${styles.container}`}>
<TranscriptAnnouncement />
<ScrollTrackerContainer>
<Row>
<ScrollTrackingItem
Expand Down
2 changes: 1 addition & 1 deletion components/OurTeam/AdvisoryBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const AdvisoryBoard = () => {
<MemberItem name="Marci Harris" descr={t("advisory.MHarris")} />
<Divider />
<MemberItem
name="Jake Hirsh-Allen"
name="Jake Hirsch-Allen"
descr={t("advisory.JakeHirshAllen")}
/>
<Divider />
Expand Down
30 changes: 20 additions & 10 deletions components/hearing/HearingSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import Link from "next/link"
import { useCallback, useEffect, useState } from "react"
import type { ModalProps } from "react-bootstrap"
import styled from "styled-components"
import Papa from "papaparse"
import { Col, Image, Modal, Row } from "../bootstrap"
import { firestore } from "../firebase"
import * as links from "../links"
import { billSiteURL, Internal } from "../links"
import { LabeledIcon } from "../shared"
import { Paragraph, formatMilliseconds } from "./hearing"
import { Paragraph, formatVTTTimestamp } from "./hearing"

type Bill = {
BillNumber: string
Expand Down Expand Up @@ -144,7 +143,7 @@ export const HearingSidebar = ({
dateCheck = true
}

const [downloadName, setDownloadName] = useState<string>("hearing.csv")
const [downloadName, setDownloadName] = useState<string>("hearing.vtt")
const [downloadURL, setDownloadURL] = useState<string>("")
const [houseChairName, setHouseChairName] = useState<string>("")
const [houseChairperson, setHouseChairperson] = useState<Legislator>()
Expand Down Expand Up @@ -187,17 +186,28 @@ export const HearingSidebar = ({
}, [committeeCode, generalCourtNumber])

useEffect(() => {
setDownloadName(`hearing-${hearingId}.csv`)
setDownloadName(`hearing-${hearingId}.vtt`)
}, [hearingId])

useEffect(() => {
if (!transcriptData) return
const csv_objects = transcriptData.map(doc => ({
start: formatMilliseconds(doc.start),
text: doc.text
}))
const csv = Papa.unparse(csv_objects)
const blob = new Blob([csv], { type: "text/csv" })
const vttLines = ["WEBVTT", ""]

transcriptData.forEach((paragraph, index) => {
const cueNumber = index + 1
const startTime = formatVTTTimestamp(paragraph.start)
const endTime = formatVTTTimestamp(paragraph.end)

vttLines.push(
String(cueNumber),
`${startTime} --> ${endTime}`,
paragraph.text,
""
)
})

const vtt = vttLines.join("\n")
const blob = new Blob([vtt], { type: "text/vtt" })
const url = URL.createObjectURL(blob)
setDownloadURL(url)

Expand Down
15 changes: 15 additions & 0 deletions components/hearing/hearing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,18 @@ export function formatMilliseconds(ms: number): string {
return `${formattedMinutes}:${formattedSeconds}`
}
}

export function formatVTTTimestamp(ms: number): string {
const totalSeconds = Math.floor(ms / 1000)
const milliseconds = 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")
const formattedMilliseconds = String(milliseconds).padStart(3, "0")

return `${formattedHours}:${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`
}
7 changes: 7 additions & 0 deletions components/search/hearings/HearingHit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export const HearingHit = ({ hit }: { hit: HearingHitData }) => {
navigateToHearing()
}

var yesterday = new Date()
yesterday.setDate(yesterday.getDate() - 1)

return (
<StyledCard
role="link"
Expand All @@ -100,6 +103,10 @@ export const HearingHit = ({ hit }: { hit: HearingHitData }) => {
<Badge bg="success" pill>
{t("video_available", { ns: "search" })}
</Badge>
) : startsAt > yesterday ? (
<Badge bg="info" text="dark" pill>
{t("video_upcoming", { ns: "search" })}
</Badge>
) : (
<Badge bg="info" text="dark" pill>
{t("video_unavailable", { ns: "search" })}
Expand Down
13 changes: 12 additions & 1 deletion components/search/hearings/HearingSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,10 @@ export const HearingSearch = () => {
currentRefinementsProps={{ excludedAttributes: ["startsAt"] }}
initialUiState={{
[sortOptions[0].value]: {
refinementList: { court: [String(CURRENT_COURT_NUMBER)] }
refinementList: {
court: [String(CURRENT_COURT_NUMBER)],
hasVideo: ["true"]
}
}
}}
searchParameters={{
Expand All @@ -104,6 +107,14 @@ export const HearingSearch = () => {
}))
.sort((a, b) => Number(b.value) - Number(a.value))
},
{
attribute: "hasVideo",
transformItems: items =>
items.map(item => ({
...item,
label: item.value === "true" ? "Yes" : "No"
}))
},
{ attribute: "committeeName" },
{ attribute: "month" },
{ attribute: "year" },
Expand Down
6 changes: 6 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"about": "About",
"announcement": {
"headingBold": "New on MAPLE:",
"headingBody": "Looking to learn about the latest action in a committee?",
"description": "MAPLE now has searchable transcripts for all legislative hearings.",
"link": "Try it out!"
},
"back_to_bills": "back to list of bills",
"back_to_hearings": "back to list of hearings",
"bill": {
Expand Down
4 changes: 3 additions & 1 deletion public/locales/en/search.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
},
"hearing": {
"court": "Legislative Session",
"hasVideo": "Video Available",
"month": "Month",
"year": "Year",
"chairNames": "Chair",
Expand Down Expand Up @@ -49,7 +50,8 @@
},
"search_error": "Something went wrong. Please try again. Original message: {{error}}",
"video_available": "Video available",
"video_unavailable": "Video and transcript not yet available",
"video_upcoming": "Video not yet posted by the legislature; transcript will be generated once it is available",
"video_unavailable": "Not available: Video and transcript",
"location_label": "Location",
"agenda_label": "Agenda Topics",
"bills_label": "Bills",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { StoryObj } from "@storybook/react"
import AnnouncementBanner from "components/AnnouncementBanner/AnnouncementBanner"
import { createMeta } from "stories/utils"

export default createMeta({
title: "components/AnnouncementBanner",
component: AnnouncementBanner
})

type Story = StoryObj<typeof AnnouncementBanner>

export const Primary: Story = {
args: {
endDate: new Date("2026-03-01T12:00:00.000Z")
},
name: "AnnouncementBanner"
}