diff --git a/components/EditProfilePage/PersonalInfoTab.tsx b/components/EditProfilePage/PersonalInfoTab.tsx index b6d70364c..111aa5038 100644 --- a/components/EditProfilePage/PersonalInfoTab.tsx +++ b/components/EditProfilePage/PersonalInfoTab.tsx @@ -10,7 +10,7 @@ import { TooltipButton } from "components/buttons" import { useTranslation } from "next-i18next" import styled from "styled-components" -type UpdateProfileData = { +export type UpdateProfileData = { fullName: string aboutYou: string twitter: string @@ -37,7 +37,7 @@ type Props = { legislatorsProps?: YourLegislatorsProps } -async function updateProfile( +export async function updateProfile( { profile, actions, uid }: Props, data: UpdateProfileData ) { diff --git a/components/LegislatorProfile/LegislatorComponents.tsx b/components/LegislatorProfile/LegislatorComponents.tsx index 1f25f37ab..38a48dbe2 100644 --- a/components/LegislatorProfile/LegislatorComponents.tsx +++ b/components/LegislatorProfile/LegislatorComponents.tsx @@ -130,6 +130,21 @@ export function LinkedIn() { ) } +export function Mastodon() { + return ( + + + + ) +} + export function Twitter() { return ( diff --git a/components/LegislatorProfile/LegislatorProfilePage.tsx b/components/LegislatorProfile/LegislatorProfilePage.tsx index 2f8d127e2..820adf4e1 100644 --- a/components/LegislatorProfile/LegislatorProfilePage.tsx +++ b/components/LegislatorProfile/LegislatorProfilePage.tsx @@ -1,9 +1,12 @@ +import { collection, getDocs, query, where } from "firebase/firestore" 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 { useEffect, useState } from "react" import styled from "styled-components" +import { firestore } from "../firebase" import * as links from "../links" import { @@ -11,6 +14,7 @@ import { DistrictLabel, formatPhoneNumber, LinkedIn, + Mastodon, PartyLabel, Twitter } from "./LegislatorComponents" @@ -24,25 +28,6 @@ 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", - "Bills", - "Elections", - "Finance", - "District", - "Her testimony", - "Votes" -] - const ButtonContainer = styled(Col).attrs(props => ({ className: `col-12 justify-content-md-end ${props.className}`, md: `3`, @@ -124,6 +109,7 @@ export function LegislatorProfilePage({ court: number memberCode: string }) { + const { user } = useAuth() const { member, loading: memberLoading } = useMember(court, memberCode) const { district, loading: districtLoading } = useDistrict( court, @@ -131,24 +117,30 @@ export function LegislatorProfilePage({ 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: "" + + const [legislatorId, setLegislatorId] = useState("") + const [legislatorData, setLegislatorData] = useState([]) + + async function getLegislatorUID(memberCode: string) { + let docList: any[] = [] + const q = query( + collection(firestore, "profiles"), + where("memberId", "==", memberCode) + ) + const docData = await getDocs(q) + + docData.forEach(doc => { + // doc.data() is never undefined for query doc snapshots + docList.push(doc.data()) + setLegislatorData(docList) + setLegislatorId(doc.id) + }) } + useEffect(() => { + getLegislatorUID(memberCode) + }, [memberCode]) + if (memberLoading) { return ( @@ -221,14 +213,14 @@ export function LegislatorProfilePage({ - {profile.website ? ( + {legislatorData[0]?.website ? (
· - {profile.website} + {legislatorData[0].website}
) : ( @@ -252,17 +244,18 @@ export function LegislatorProfilePage({ )}
- {profile?.social?.twitter || - profile?.social?.linkedIn || - profile?.social?.blueSky ? ( + {legislatorData[0]?.social.twitter || + legislatorData[0]?.social.linkedIn || + legislatorData[0]?.social.blueSky || + legislatorData[0]?.social.mastodon ? ( · ) : ( <> )} - {profile?.social?.twitter ? ( + {legislatorData[0]?.social.twitter ? ( )} - {profile?.social?.linkedIn ? ( + + {legislatorData[0]?.social.linkedIn ? ( )} - {profile?.social?.blueSky ? ( + + {legislatorData[0]?.social.blueSky ? ( )} + + {legislatorData[0]?.social.mastodon ? ( + + + + ) : ( + <> + )}
- {/* uncomment when legislator Maple accounts are linked to this page - - {user && legislatorAccountId ? ( + {user && legislatorId ? (
- +
) : ( <> - )} */} + )} - + diff --git a/components/LegislatorProfile/LegislatorSidebar.tsx b/components/LegislatorProfile/LegislatorSidebar.tsx index 96cf42c9d..c35d166f4 100644 --- a/components/LegislatorProfile/LegislatorSidebar.tsx +++ b/components/LegislatorProfile/LegislatorSidebar.tsx @@ -2,12 +2,27 @@ import { Biography } from "./SidebarComponents/Biography" import { OtherTestimony } from "./SidebarComponents/OtherTestimony" import { UpcomingHearings } from "./SidebarComponents/UpcomingHearings" -export function LegislatorSidebar() { +export function LegislatorSidebar({ + court, + legislatorData, + legislatorId, + memberCode +}: { + court: number + legislatorData: any[] + legislatorId: string + memberCode: string +}) { return ( <> - + ) } diff --git a/components/LegislatorProfile/LegislatorTabs.tsx b/components/LegislatorProfile/LegislatorTabs.tsx index 40c264453..6f2791f19 100644 --- a/components/LegislatorProfile/LegislatorTabs.tsx +++ b/components/LegislatorProfile/LegislatorTabs.tsx @@ -66,10 +66,14 @@ const TabNavItem = ({ export function LegislatorTabs({ district, districtLoading, + legislatorId, + name, tabCategory }: { district?: District | undefined districtLoading?: boolean + legislatorId: string + name: string tabCategory?: TabCategories }) { const { t } = useTranslation("legislators") @@ -103,7 +107,7 @@ export function LegislatorTabs({ { title: t("tabs.testimony"), eventKey: "testimony", - content: + content: }, { title: t("tabs.votes"), diff --git a/components/LegislatorProfile/SidebarComponents/Biography.tsx b/components/LegislatorProfile/SidebarComponents/Biography.tsx index e190ac443..cdbed8755 100644 --- a/components/LegislatorProfile/SidebarComponents/Biography.tsx +++ b/components/LegislatorProfile/SidebarComponents/Biography.tsx @@ -1,5 +1,149 @@ -import { TabBlock } from "../LegislatorComponents" +import { useTranslation } from "next-i18next" +import { useEffect, useState } from "react" +import { useForm } from "react-hook-form" +import styled from "styled-components" -export function Biography() { - return - Biography +import { Form } from "../../bootstrap" +import { Profile, ProfileHook, useProfile } from "../../db" +import Input from "../../forms/Input" + +import { useAuth } from "components/auth" +import { + updateProfile, + UpdateProfileData +} from "components/EditProfilePage/PersonalInfoTab" + +const BioBlock = styled.div` + background-color: white; + border-color: #b8c0c9; + border-radius: 5px; + border-style: solid; + border-width: 1px; + font-size: 11px; + padding: 16px; +` + +const BioButton = styled.button` + font-size: 9px; + padding: 2px; +` + +const BioTitle = styled.div` + font-weight: 700; + color: #0b0a3e; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 10px; +` + +export function Biography({ + court, + legislatorData, + legislatorId, + memberCode +}: { + court: number + legislatorData: any[] + legislatorId: string + memberCode: string +}) { + const { user } = useAuth() + const uid = user?.uid + + let pageOwner = false + if (uid === legislatorId) { + pageOwner = true + } + + const userResult = useProfile() + + if (userResult.profile && pageOwner) { + // the user is the legislator who owns this page + // therefore they get edit privledges + return ( + + ) + } + + // the user is not the legislator who owns this page + // therefore they get read-only privledges + return +} + +function EditableBiography({ + actions, + court, + memberCode, + profile +}: { + actions: ProfileHook + court: number + memberCode: string + profile: Profile +}) { + const { + register, + formState: { errors, isDirty }, + handleSubmit + } = useForm() + + const { about }: Profile = profile + + const onSubmit = handleSubmit(async update => { + await updateProfile({ profile, actions }, update) + location.assign(`/legislators/${court}/${memberCode}`) + setFormUpdated(false) + }) + + const { t } = useTranslation("legislators") + const [formUpdated, setFormUpdated] = useState(false) + + useEffect(() => { + setFormUpdated(isDirty) + }, [isDirty, setFormUpdated]) + + return ( + +
+
+ + {t("biography")} + + + {t("submit")} + +
+ +
+
+ ) +} + +function ReadonlyBiography({ legislatorData }: { legislatorData: any[] }) { + const { t } = useTranslation("legislators") + + return ( + + {t("biography")} +
+ {legislatorData[0]?.about ? legislatorData[0].about : t("notClaimed")} +
+
+ ) } diff --git a/components/LegislatorProfile/TabComponents/TestimonyTab.tsx b/components/LegislatorProfile/TabComponents/TestimonyTab.tsx index eb3bc6e04..74f6383fa 100644 --- a/components/LegislatorProfile/TabComponents/TestimonyTab.tsx +++ b/components/LegislatorProfile/TabComponents/TestimonyTab.tsx @@ -1,5 +1,98 @@ +import { useMemo } from "react" +import { useTranslation } from "next-i18next" +import styled from "styled-components" + import { TabBlock } from "../LegislatorComponents" -export function TestimonyTab() { - return - Testimony +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-color: #b8c0c9; + border-radius: 5px; + border-style: solid; + border-width: 1px; + color: #1a3185; + display: flex; + font-size: 13px; + gap: 10px; + line-height: 1.6; + margin-top: 14px; + margin-bottom: 14px; + padding: 12px 16px; +` + +function Disclaimer({ fullname }: { fullname?: string }) { + const { t } = useTranslation("legislators") + + return ( + + + + + + +
+ {fullname} {t("canSubmit")} +
+
+ ) +} + +export function TestimonyTab({ + legislatorId, + name +}: { + legislatorId: string + name: string +}) { + const { t } = useTranslation("testimony") + const { user } = useAuth() + + const testimony = usePublishedTestimonyListing({ + uid: legislatorId + }) + + const testimonies = useMemo(() => { + const legislatorTestimonies = testimony.items.result ?? [] + + // 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 ( + <> + + {testimonies.length > 0 && legislatorId ? ( +
+ {testimonies.map(testimony => ( + + + + ))} +
+ ) : ( + + {t("viewTestimony.noTestimonies")} + + )} + + ) } diff --git a/components/LegislatorProfile/TabComponents/TestimonyTab_PendingProfileLink.tsx b/components/LegislatorProfile/TabComponents/TestimonyTab_PendingProfileLink.tsx deleted file mode 100644 index 12b8174fd..000000000 --- a/components/LegislatorProfile/TabComponents/TestimonyTab_PendingProfileLink.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { useMemo } from "react" -import { useTranslation } from "next-i18next" -import styled from "styled-components" - -import { TabBlock } from "../LegislatorComponents" - -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; -` - -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/db/profile/urlCleanup.ts b/components/db/profile/urlCleanup.ts index 26d09b13a..f2fe17e5c 100644 --- a/components/db/profile/urlCleanup.ts +++ b/components/db/profile/urlCleanup.ts @@ -7,7 +7,7 @@ export function cleanSocialLinks(network: keyof SocialLinks, link: string) { const index: number = path.indexOf(".com/") + 5 path = path.substring(index) } - if (network === "mastodon" && path.startsWith("@")) { + if (path && network === "mastodon" && path.startsWith("@")) { path = path.substring(1) } } diff --git a/public/locales/en/legislators.json b/public/locales/en/legislators.json index 886f6a1ba..4a9e69920 100644 --- a/public/locales/en/legislators.json +++ b/public/locales/en/legislators.json @@ -1,13 +1,17 @@ { + "addBio": "Add your biography", "billsSponsored": "Bills Sponsored", - "canSubmit": "can submit testimony on any bill or ballot question, just like any constituent. Her stance and reasoning are shown exactly as submitted — MAPLE does not edit or editorialize.", + "biography": "Biography", + "canSubmit": "can submit testimony on any bill or ballot question, just like any constituent. Their stance and reasoning are shown exactly as submitted — MAPLE does not edit or editorialize.", "contact": "Contact", "cosponsored": "Cosponsored", "districtDetails": "District details are not available yet.", + "editBio": "Edit your biography", "fundsRaised": "Funds Raised", "home": "Home", "legislators": "Legislators", "loading": "Loading district...", + "notClaimed": "This legislator has not claimed their MAPLE account", "party": { "democratic": "Democratic Party", "party": "Party", @@ -15,6 +19,7 @@ }, "stateRepresentative": "State Representative", "stateSenator": "State Senator", + "submit": "Submit", "tabs": { "bills": "Bills", "district": "District",