diff --git a/components/LegislatorProfile/LegislatorComponents.tsx b/components/LegislatorProfile/LegislatorComponents.tsx new file mode 100644 index 000000000..e420630f4 --- /dev/null +++ b/components/LegislatorProfile/LegislatorComponents.tsx @@ -0,0 +1,131 @@ +import { useTranslation } from "next-i18next" +import styled from "styled-components" + +export const formatPhoneNumber = (value: string) => { + if (!value) return value + + const phoneNumber = value.replace(/[^\d]/g, "") + const phoneNumberLength = phoneNumber.length + + // Format as (XXX) XXX-XXXX + if (phoneNumberLength < 4) return phoneNumber + if (phoneNumberLength < 7) { + return `(${phoneNumber.slice(0, 3)}) ${phoneNumber.slice(3)}` + } + return ` + (${phoneNumber.slice(0, 3)}) + ${phoneNumber.slice(3, 6)}- + ${phoneNumber.slice(6, 10)} + ` +} + +/** Party Labels **/ + +const DemocraticBubble = styled.div.attrs(props => ({ + className: `${props.className}` +}))` + background: #d1d6e7; + color: #1a3185; + font-size: 11px; + font-weight: 700; + padding: 1px 10px; + border-radius: 999px; + width: max-content; +` + +const DistrictBubble = styled.div.attrs(props => ({ + className: `ms-1 ${props.className}` +}))` + background: #fff3cd; + color: #856404; + font-size: 11px; + font-weight: 700; + padding: 1px 10px; + border-radius: 999px; + width: max-content; +` + +const IndependantBubble = styled.div.attrs(props => ({ + className: `${props.className}` +}))` + background: #bca0dc; + color: #3c1361; + font-size: 11px; + font-weight: 700; + padding: 1px 10px; + border-radius: 999px; + width: max-content; +` + +const RepublicanBubble = styled.div.attrs(props => ({ + className: `${props.className}` +}))` + background: #f29999; + color: #de0100; + font-size: 11px; + font-weight: 700; + padding: 1px 10px; + border-radius: 999px; + width: max-content; +` + +export function DistrictLabel(props: { district: string }) { + return {props.district} +} + +export function PartyLabel(props: { party: string }) { + const { t } = useTranslation("legislators") + + switch (props.party) { + case "Democrat": + return {t("party.democratic")} + case "Republican": + return {t("party.republican")} + default: + return ( + + {props.party} {t("party.party")} + + ) + } +} + +/** Social Media components **/ + +export function Bluesky() { + return ( + + + + ) +} + +export function LinkedIn() { + return ( + + + + ) +} + +export function Twitter() { + return ( + + + + ) +} diff --git a/components/LegislatorProfile/LegislatorProfilePage.tsx b/components/LegislatorProfile/LegislatorProfilePage.tsx index 34a0d4ed8..4a0e22df5 100644 --- a/components/LegislatorProfile/LegislatorProfilePage.tsx +++ b/components/LegislatorProfile/LegislatorProfilePage.tsx @@ -1,8 +1,37 @@ +import { faChevronRight } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import ErrorPage from "next/error" +import { useTranslation } from "next-i18next" import styled from "styled-components" + +import * as links from "../links" + +import { + Bluesky, + DistrictLabel, + formatPhoneNumber, + LinkedIn, + PartyLabel, + Twitter +} from "./LegislatorComponents" +// import { LegislatorSidebar } from "./SidebarComponents/LegislatorSidebar" +import { LegislatorTabs } from "./LegislatorTabs" + +import { useAuth } from "components/auth" import { Col, Container, Row, Spinner } from "components/bootstrap" import { useDistrict, useMember } from "components/db" -import { DistrictTab } from "./DistrictTab" +import { Internal } from "components/links" +import { FollowUserButton } from "components/shared/FollowButton" +import { CircleImage } from "components/shared/LabeledIcon" + +type ProfilePlaceholder = { + social?: { + blueSky?: string + linkedIn?: string + twitter?: string + } + website?: string +} const tabs = [ "Priorities", @@ -14,21 +43,95 @@ const tabs = [ "Votes" ] -const TabButton = styled.button` - background: transparent; - border: 0; - border-bottom: 5px solid transparent; - color: #68707a; - font-size: 1.35rem; +const ButtonContainer = styled(Col).attrs(props => ({ + className: `col-12 justify-content-md-end ${props.className}`, + md: `3`, + sm: `4` +}))` + width: max-content; +` + +const DirectoryPath = styled.div.attrs(props => ({ + className: `align-items-center d-flex flex-nowrap ${props.className}` +}))` + font-size: 12px; +` + +const HeaderBlock = styled.div.attrs(props => ({ + className: `d-flex flex-wrap justify-content-between ${props.className}` +}))` + background-color: white; + border: "1px #ced4da solid"; + border-radius: 5px; + margin-top: 8px; + padding: 16px; +` + +const HeaderName = styled.div` + font-size: 26px; font-weight: 700; - padding: 1.35rem 1.9rem 1.15rem; + color: #0b0a3e; +` - &.active { - border-bottom-color: #18358f; - color: #18358f; - } +const RoleLine = styled.div.attrs(props => ({ + className: `mb-2 ${props.className}` +}))` + color: #6c757d; + font-size: 14px; +` + +const PhoneNum = styled.span` + color: #6c757d; +` + +const SocialLine = styled.div.attrs(props => ({ + className: `d-flex flex-wrap mb-2 ${props.className}` +}))` + font-size: 12px; + text-decoration: none; +` + +const StatBlock = styled(Col).attrs(props => ({ + className: `d-flex col-4 flex-grow-1 ${props.className}`, + md: `2` +}))` + background-color: white; + border: 1px #ced4da solid; + border-radius: 5px; + margin-top: 4px; + padding: 16px; ` +const StatLine = styled(Row).attrs(props => ({ + className: `text-nowrap ${props.className}` +}))` + font-size: 12px; +` + +const StatNum = styled.div.attrs(props => ({ + className: `mx-auto ${props.className}` +}))` + color: #1a3185; + font-size: 22px; + font-weight: 700; + width: max-content; +` + +// const TabButton = styled.button` +// background: transparent; +// border: 0; +// border-bottom: 5px solid transparent; +// color: #68707a; +// font-size: 1.35rem; +// font-weight: 700; +// padding: 1.35rem 1.9rem 1.15rem; + +// &.active { +// border-bottom-color: #18358f; +// color: #18358f; +// } +// ` + export function LegislatorProfilePage({ court, memberCode @@ -42,6 +145,24 @@ export function LegislatorProfilePage({ member?.Branch, member?.District ) + const { t } = useTranslation("legislators") + // const { user } = useAuth() **uncomment when Following Button in enabled** + + /* replace with profile info for legislators with Maple accounts */ + let profile: ProfilePlaceholder = { + // social: { + // blueSky: "blueskyTest", + // linkedIn: "linkedinTest", + // twitter: "twitterTest" + // }, + // website: "test.com" + social: { + blueSky: "", + linkedIn: "", + twitter: "" + }, + website: "" + } if (memberLoading) { return ( @@ -55,17 +176,210 @@ export function LegislatorProfilePage({ return } + console.log("district: ", district) + return ( - - + + + + {t("home")} + + + + {/* update with link to legistators search page when that is created */} +
{t("legislators")}
+ {/* */} + + +
{member.Name}
+
+ + + + {""} + + -

{member.Name}

-

- {member.Branch} - {member.District} -

+ + + {member.Name} + + + + + {member.Branch == "Senate" ? ( + {t("stateSenator")} + ) : ( + {t("stateRepresentative")} + )} + {/* · Town */} + + +
+ + {/* Incumbent Label */} + +
+ + +
+ + {member.EmailAddress} + +
+ + {profile.website ? ( +
+ · + + {profile.website} + +
+ ) : ( +
+ · + + {`malegislature.gov/Legislators/Profile/${member.MemberCode}`} + +
+ )} + + {member.PhoneNumber ? ( +
+ · + {formatPhoneNumber(member.PhoneNumber)} +
+ ) : ( + <> + )} + +
+ {profile?.social?.twitter || + profile?.social?.linkedIn || + profile?.social?.blueSky ? ( + · + ) : ( + <> + )} + + {profile?.social?.twitter ? ( + + + + ) : ( + <> + )} + {profile?.social?.linkedIn ? ( + + + + ) : ( + <> + )} + {profile?.social?.blueSky ? ( + + + + ) : ( + <> + )} +
+
+ + + + {/* uncomment when legislator Maple accounts are linked to this page + + {user && legislatorAccountId ? ( +
+ +
+ ) : ( + <> + )} */} + + {t("contact")} + +
+
+ +
+ + + ? + {t("termsServed")} + + + + + {member.SponsoredBills.length} + {t("billsSponsored")} + + + + + {member.CoSponsoredBills.length} + {t("cosponsored")} + + + + + ? + {t("fundsRaised")} + + +
+ + + + + + + {/* */} +
Sidebar
-
-
+
*/}
) } diff --git a/components/LegislatorProfile/LegislatorTabs.tsx b/components/LegislatorProfile/LegislatorTabs.tsx new file mode 100644 index 000000000..b969ad04c --- /dev/null +++ b/components/LegislatorProfile/LegislatorTabs.tsx @@ -0,0 +1,133 @@ +import { useTranslation } from "next-i18next" +import { TabPane } from "react-bootstrap" +import TabContainer from "react-bootstrap/TabContainer" +import styled from "styled-components" + +import { Container, Nav } from "../bootstrap" + +import { BillsTab } from "./TabComponents/BillsTab" +import { DistrictTab } from "./TabComponents/DistrictTab" +import { ElectionsTab } from "./TabComponents/ElectionsTab" +import { FinanceTab } from "./TabComponents/FinanceTab" +import { PrioritiesTab } from "./TabComponents/PrioritiesTab" +import { TestimonyTab } from "./TabComponents/TestimonyTab" +import { VotesTab } from "./TabComponents/VotesTab" + +import { District } from "components/db" +import { + StyledTabContent, + TabNavWrapper, + TabType +} from "components/EditProfilePage/StyledEditProfileComponents" + +const tabCategory = [ + "priorities", + "bills", + "elections", + "finance", + "district", + "testimony", + "votes" +] +type TabCategories = (typeof tabCategory)[number] + +const TabNavLink = styled(Nav.Link).attrs(props => ({ + className: `rounded-top m-0 p-0 ${props.className}` +}))` + color: #6c757d; + + &.active { + color: #1a3185; + font-weight: bold; + } +` + +const TabNavItem = ({ + tab, + i: i, + className +}: { + tab: TabType + i: number + className?: string +}) => { + return ( + + +

+ {tab.title} +

+
+
+
+ ) +} + +export function LegislatorTabs({ + district, + districtLoading, + tabCategory +}: { + district?: District | undefined + districtLoading?: boolean + tabCategory?: TabCategories +}) { + const { t } = useTranslation("legislators") + + const tabs = [ + { + title: t("tabs.priorities"), + eventKey: "priorities", + content: + }, + { + title: t("tabs.bills"), + eventKey: "bills", + content: + }, + { + title: t("tabs.elections"), + eventKey: "elections", + content: + }, + { + title: t("tabs.finance"), + eventKey: "finance", + content: + }, + { + title: t("tabs.district"), + eventKey: "district", + content: + }, + { + title: t("tabs.testimony"), + eventKey: "testimony", + content: + }, + { + title: t("tabs.votes"), + eventKey: "votes", + content: + } + ] + + return ( + + + + {tabs.map((t, i) => ( + + ))} + + + {tabs.map(t => ( + + {t.content} + + ))} + + + + ) +} diff --git a/components/LegislatorProfile/TabComponents/BillsTab.tsx b/components/LegislatorProfile/TabComponents/BillsTab.tsx new file mode 100644 index 000000000..7ccfc2ebd --- /dev/null +++ b/components/LegislatorProfile/TabComponents/BillsTab.tsx @@ -0,0 +1,3 @@ +export function BillsTab() { + return
- Bills
+} diff --git a/components/LegislatorProfile/DistrictTab.test.tsx b/components/LegislatorProfile/TabComponents/DistrictTab.test.tsx similarity index 100% rename from components/LegislatorProfile/DistrictTab.test.tsx rename to components/LegislatorProfile/TabComponents/DistrictTab.test.tsx diff --git a/components/LegislatorProfile/DistrictTab.tsx b/components/LegislatorProfile/TabComponents/DistrictTab.tsx similarity index 100% rename from components/LegislatorProfile/DistrictTab.tsx rename to components/LegislatorProfile/TabComponents/DistrictTab.tsx diff --git a/components/LegislatorProfile/TabComponents/DistrictTabOld.tsx b/components/LegislatorProfile/TabComponents/DistrictTabOld.tsx new file mode 100644 index 000000000..b2a70fa19 --- /dev/null +++ b/components/LegislatorProfile/TabComponents/DistrictTabOld.tsx @@ -0,0 +1,3 @@ +export function DistrictTab() { + return
- District
+} diff --git a/components/LegislatorProfile/TabComponents/ElectionsTab.tsx b/components/LegislatorProfile/TabComponents/ElectionsTab.tsx new file mode 100644 index 000000000..6fea4cfbf --- /dev/null +++ b/components/LegislatorProfile/TabComponents/ElectionsTab.tsx @@ -0,0 +1,3 @@ +export function ElectionsTab() { + return
- Elections
+} diff --git a/components/LegislatorProfile/TabComponents/FinanceTab.tsx b/components/LegislatorProfile/TabComponents/FinanceTab.tsx new file mode 100644 index 000000000..95f840dbb --- /dev/null +++ b/components/LegislatorProfile/TabComponents/FinanceTab.tsx @@ -0,0 +1,3 @@ +export function FinanceTab() { + return
- Finance
+} diff --git a/components/LegislatorProfile/TabComponents/PrioritiesTab.tsx b/components/LegislatorProfile/TabComponents/PrioritiesTab.tsx new file mode 100644 index 000000000..33c98c008 --- /dev/null +++ b/components/LegislatorProfile/TabComponents/PrioritiesTab.tsx @@ -0,0 +1,3 @@ +export function PrioritiesTab() { + return
- Priorities
+} diff --git a/components/LegislatorProfile/TabComponents/TestimonyTab.tsx b/components/LegislatorProfile/TabComponents/TestimonyTab.tsx new file mode 100644 index 000000000..e00fd7e0d --- /dev/null +++ b/components/LegislatorProfile/TabComponents/TestimonyTab.tsx @@ -0,0 +1,3 @@ +export function TestimonyTab() { + return
- Testimony
+} diff --git a/components/LegislatorProfile/TabComponents/TestimonyTab_PendingProfileLink.tsx b/components/LegislatorProfile/TabComponents/TestimonyTab_PendingProfileLink.tsx new file mode 100644 index 000000000..da1e672a3 --- /dev/null +++ b/components/LegislatorProfile/TabComponents/TestimonyTab_PendingProfileLink.tsx @@ -0,0 +1,102 @@ +import { useMemo } from "react" +import { useTranslation } from "next-i18next" +import styled from "styled-components" + +import { useAuth } from "components/auth" +import { usePublishedTestimonyListing } from "components/db/testimony/usePublishedTestimonyListing" +import { NoResults } from "components/search/NoResults" +import { TestimonyItem } from "components/TestimonyCard/TestimonyItem" + +const DisclaimerBlock = styled.div` + align-items: flex-start; + background-color: #f0f4ff; + border: "1px #d1d6e7 solid"; + border-radius: 5px; + color: #1a3185; + display: flex; + font-size: 13px; + gap: 10px; + line-height: 1.6; + margin-top: 14px; + margin-bottom: 14px; + padding: 12px 16px; +` + +const TestimonyBlock = styled.div` + background-color: white; + border: "1px #ced4da solid"; + border-radius: 5px; + font-size: 11px; + margin-bottom: 14px; + padding: 0px 16px; +` + +function Disclaimer({ fullname }: { fullname?: string }) { + const { t } = useTranslation("legislators") + + return ( + + + + + + +
+ {fullname} {t("canSubmit")} +
+
+ ) +} + +export function Testimony({ + fullname, + pageId +}: { + fullname?: string + pageId?: string +}) { + const { t } = useTranslation("testimony") + const { user } = useAuth() + + const testimony = usePublishedTestimonyListing({ + uid: pageId + }) + + const allTestimonies = useMemo(() => { + const legislatorTestimonies = testimony.items.result ?? [] + + // Combine and sort by publishedAt (newest first), then take 4 most recent + return [...legislatorTestimonies] + .sort((a, b) => b.publishedAt.toMillis() - a.publishedAt.toMillis()) + .slice(0, 4) + }, [testimony.items.result]) + + return ( + <> + + {allTestimonies.length > 0 ? ( +
+ {allTestimonies.map(testimony => ( + + + + ))} +
+ ) : ( + {t("viewTestimony.noTestimonies")} + )} + + ) +} diff --git a/components/LegislatorProfile/TabComponents/VotesTab.tsx b/components/LegislatorProfile/TabComponents/VotesTab.tsx new file mode 100644 index 000000000..3c6625cd2 --- /dev/null +++ b/components/LegislatorProfile/TabComponents/VotesTab.tsx @@ -0,0 +1,3 @@ +export function VotesTab() { + return
- Votes
+} diff --git a/components/LegislatorProfile/index.ts b/components/LegislatorProfile/index.ts index 056729577..01fb5e8c3 100644 --- a/components/LegislatorProfile/index.ts +++ b/components/LegislatorProfile/index.ts @@ -1,2 +1 @@ -export * from "./DistrictTab" export * from "./LegislatorProfilePage" diff --git a/components/db/members.ts b/components/db/members.ts index d4cd503bc..3025245ea 100644 --- a/components/db/members.ts +++ b/components/db/members.ts @@ -21,6 +21,8 @@ export type MemberContent = { PhoneNumber: string FaxNumber: string | null Committees: CommitteeReference[] + SponsoredBills: Array + CoSponsoredBills: Array } export type Member = { diff --git a/pages/legislators/[court]/[memberCode].tsx b/pages/legislators/[court]/[memberCode].tsx index 5824626c9..5c6357718 100644 --- a/pages/legislators/[court]/[memberCode].tsx +++ b/pages/legislators/[court]/[memberCode].tsx @@ -1,8 +1,9 @@ import { GetServerSideProps } from "next" import { serverSideTranslations } from "next-i18next/serverSideTranslations" import { z } from "zod" -import { LegislatorProfilePage } from "components/LegislatorProfile" + import { flags } from "components/featureFlags" +import { LegislatorProfilePage } from "components/LegislatorProfile" import { createPage } from "components/page" const Query = z.object({ @@ -14,7 +15,7 @@ export default createPage<{ court: number memberCode: string }>({ - titleI18nKey: "titles.legislatorProfile", + titleI18nKey: "navigation.legislator", Page: ({ court, memberCode }) => ( ) @@ -38,7 +39,14 @@ export const getServerSideProps: GetServerSideProps = async ctx => { return { props: { ...query.data, - ...(await serverSideTranslations(locale, ["auth", "common", "footer"])) + ...(await serverSideTranslations(locale, [ + "auth", + "common", + "footer", + "legislators", + "profile", + "testimony" + ])) } } }