diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..25add1752 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +# Environment Variables for Local Development +TEST_ADMIN_USERNAME=your_admin_email@example.com +TEST_ADMIN_PASSWORD=your_admin_password +APP_API_URL=http://localhost:3000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0f2c58705..37bd8e8b0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @sashamaryl @mertbagt @nesanders @mvictor55 @timblais @alexjball @Mephistic +* @sashamaryl @mertbagt @nesanders @mvictor55 @timblais @alexjball @Mephistic @kiminkim724 diff --git a/.gitignore b/.gitignore index b1816690d..3151a5f59 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ /playwright-report/ /blob-report/ /playwright/.cache/ +.env .next/ .eslintcache diff --git a/.prettierignore b/.prettierignore index 66c8c1f60..417e4f5b7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,4 +12,5 @@ analysis/data dist *.handlebars coverage -storybook-static \ No newline at end of file +storybook-static +llm diff --git a/README.md b/README.md index ff58a96d1..61ce3106c 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,25 @@ Install the [Redux DevTools](https://chrome.google.com/webstore/detail/redux-dev MAPLE uses Jest for unit and integration testing, and Playwright for e2e testing. -To start running tests, use one of the following commands: +Environment Setup for Testing. + +To set up your environment for testing, make sure you have a .env file configured with the necessary variables. You can create it by copying the provided .env.example template: + +``` +cp .env.example .env +``` + +This file includes placeholders for key environment variables, which you should customize as needed: + +``` +TEST_ADMIN_USERNAME: Username for admin testing. +TEST_ADMIN_PASSWORD: Password for admin testing. +APP_API_URL: The base URL for the application API (default is http://localhost:3000). +``` + +Running Tests. + +Once your environment is set up, you can start running tests with one of the following commands: - `yarn test:integration [--watch] [-t testNamePattern] [my/feature.test.ts]`: Run integration tests in `components/` and `tests/integration/`. These tests run against the full local application -- start it with `yarn up`. You can use `--watch` to rerun your tests as you change them and filter by test name and file. - `yarn test:e2e`: Run e2e tests in `tests/e2e` with the Playwright UI diff --git a/components/AboutSectionInfoCard/AboutInfoCard.tsx b/components/AboutSectionInfoCard/AboutInfoCard.tsx index 49aac05ff..3f7b42d9c 100644 --- a/components/AboutSectionInfoCard/AboutInfoCard.tsx +++ b/components/AboutSectionInfoCard/AboutInfoCard.tsx @@ -21,7 +21,7 @@ export default function AboutInfoCard({ title, bodytext }: AboutInfoCardProps) { {title} diff --git a/components/AlertCard/AlertCard.tsx b/components/AlertCard/AlertCard.tsx deleted file mode 100644 index a4ead2015..000000000 --- a/components/AlertCard/AlertCard.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { CardTitle } from "components/Card" -import { Timestamp } from "firebase/firestore" -import { Card as MapleCard } from "../Card/Card" -import { AlertCardBody } from "./AlertCardBody" - -export const AlertCard = (props: { - header: string - subheader: string - timestamp: Timestamp - headerImgSrc: string - headerImgTitle?: string - bodyImgSrc: string - bodyImgAltTxt: string - bodyText: string -}) => { - const date = props.timestamp.toDate() - const formattedTimestamp = `${date.toLocaleDateString()}, ${date.toLocaleTimeString()}` - const header = ( - - ) - - const body = ( - - ) - - return -} diff --git a/components/AlertCard/AlertCardBody.tsx b/components/AlertCard/AlertCardBody.tsx deleted file mode 100644 index 15612489e..000000000 --- a/components/AlertCard/AlertCardBody.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { ReactElement } from "react" -import CardBootstrap from "react-bootstrap/Card" - -interface AlertCardBodyProps { - imgSrc?: string - imgAltTxt?: string - text: string -} - -export const AlertCardBody = (props: AlertCardBodyProps) => { - const { imgSrc, imgAltTxt, text } = props - return ( -
- {imgSrc && {imgAltTxt}} - - {text} - -
- ) -} diff --git a/components/Card/CardTitle.tsx b/components/Card/CardTitle.tsx index 81dda235a..1d97d23bb 100644 --- a/components/Card/CardTitle.tsx +++ b/components/Card/CardTitle.tsx @@ -1,44 +1,212 @@ +import { useTranslation } from "next-i18next" import { ReactElement } from "react" import CardBootstrap from "react-bootstrap/Card" +import { formatBillId } from "components/formatting" +import { Internal } from "components/links" interface CardTitleProps { + authorUid?: string + billId?: string + court?: string header?: string subheader?: string timestamp?: string imgSrc?: string - imgTitle?: string inHeaderElement?: ReactElement + isBillMatch?: boolean + isUserMatch?: boolean + type?: string + userRole?: string } export const CardTitle = (props: CardTitleProps) => { - const { header, subheader, timestamp, imgSrc, imgTitle, inHeaderElement } = - props + const { + authorUid, + billId, + court, + header, + isBillMatch, + isUserMatch, + subheader, + type, + userRole + } = props + return ( - -
- {imgSrc && } -
{imgTitle}
-
- - {header && ( - - {header} - - )} - {subheader && ( - - {subheader} - - )} - {timestamp && ( - - {timestamp} - - )} + + + + + - {inHeaderElement && inHeaderElement} ) } + +const CardHeaderImg = (props: CardTitleProps) => { + const { type, userRole } = props + + let avatar = `individualUser.svg` + if (userRole == `organization`) { + avatar = `OrganizationUser.svg` + } + + switch (type) { + case "testimony": + return ( +
+ capitol building +
+ ) + case "bill": + return ( +
+ capitol building +
+ ) + default: + return <> + } +} + +const CardTitleHeadline = (props: CardTitleProps) => { + const { authorUid, billId, court, header, subheader, type } = props + const { t } = useTranslation("common") + + switch (type) { + case "testimony": + return ( + <> + {header && subheader && ( + + + {subheader} + + + {t("newsfeed.endorsed")} + + {billId && {formatBillId(billId)}} + + + )} + + ) + case "bill": + return ( + <> + {header && ( + + {billId && ( + + {formatBillId(billId)} + + )}{" "} + {subheader && ( + <> + {t("newsfeed.actionUpdate")} + {subheader} + + )} + + )} + + ) + default: + return ( + + {header} + + ) + } +} + +const CardTitleFollowing = (props: CardTitleProps) => { + const { billId, header, isBillMatch, isUserMatch, subheader, type } = props + const { t } = useTranslation("common") + + if (type == ``) { + return <> + } else if (type === `bill`) { + return ( + <> + {header && ( + + {isBillMatch ? ( + <>{t("newsfeed.follow")} + ) : ( + <>{t("newsfeed.notFollow")} + )} + {billId && {formatBillId(billId)}} + + )} + + ) + } else if (isBillMatch && isUserMatch) { + return ( + + {t("newsfeed.follow")} + {billId && {formatBillId(billId)}} + {t("newsfeed.and")} + {subheader} + + ) + } else if (isBillMatch === true && isUserMatch === false) { + return ( + + {t("newsfeed.follow")} + {billId && {formatBillId(billId)}} + + ) + } else if (isBillMatch === false && isUserMatch === true) { + return ( + + {t("newsfeed.follow")} + {subheader} + + ) + } else { + return ( + + {t("newsfeed.notFollowEither")} + {billId && {formatBillId(billId)}} + {t("newsfeed.or")} + {subheader} + + ) + } +} diff --git a/components/DesktopNav.tsx b/components/DesktopNav.tsx deleted file mode 100644 index 458f6c544..000000000 --- a/components/DesktopNav.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useTranslation } from "next-i18next" -import React from "react" -import { SignInWithButton, signOutAndRedirectToHome, useAuth } from "./auth" -import { Container, Dropdown, Nav, NavDropdown } from "./bootstrap" - -import { - Avatar, - NavbarLinkAI, - NavbarLinkBills, - NavbarLinkEditProfile, - NavbarLinkEffective, - NavbarLinkFAQ, - NavbarLinkGoals, - NavbarLinkLogo, - NavbarLinkProcess, - NavbarLinkSignOut, - NavbarLinkSupport, - NavbarLinkTeam, - NavbarLinkTestimony, - NavbarLinkViewProfile, - NavbarLinkWhyUse -} from "./NavbarComponents" - -export const DesktopNav: React.FC> = () => { - const { authenticated } = useAuth() - const { t } = useTranslation(["common", "auth"]) - - return ( - - - -
- -
- -
- -
- -
- -
- -
- - - {t("about")} - - - - - - - - - -
- -
- - - {t("learn")} - - - - - - - -
- - {authenticated ? ( -
- - - - - - - - - - - - - { - void signOutAndRedirectToHome() - }} - /> - - - -
- ) : ( -
- -
- )} -
- ) -} diff --git a/components/EditProfilePage/EditProfileHeader.tsx b/components/EditProfilePage/EditProfileHeader.tsx index 08dbf9f01..b5c02f5b7 100644 --- a/components/EditProfilePage/EditProfileHeader.tsx +++ b/components/EditProfilePage/EditProfileHeader.tsx @@ -1,7 +1,8 @@ import { useTranslation } from "next-i18next" import { Role } from "../auth" import { Col, Row } from "../bootstrap" -import { FillButton, GearIcon, OutlineButton } from "../buttons" +import { GearIcon, OutlineButton } from "../buttons" +import { ProfileEditToggle } from "components/ProfilePage/ProfileButtons" export const EditProfileHeader = ({ formUpdated, @@ -28,15 +29,7 @@ export const EditProfileHeader = ({ Icon={GearIcon} onClick={() => onSettingsModalOpen()} /> - + ) diff --git a/components/EditProfilePage/EditProfilePage.tsx b/components/EditProfilePage/EditProfilePage.tsx index d0ed0a249..de6b02c57 100644 --- a/components/EditProfilePage/EditProfilePage.tsx +++ b/components/EditProfilePage/EditProfilePage.tsx @@ -1,7 +1,5 @@ -import { useFlags } from "components/featureFlags" -import { PendingUpgradeBanner } from "components/PendingUpgradeBanner" import { useTranslation } from "next-i18next" -import { useState } from "react" +import { useContext, useState } from "react" import { TabPane } from "react-bootstrap" import TabContainer from "react-bootstrap/TabContainer" import { useAuth } from "../auth" @@ -25,6 +23,9 @@ import { TabNavWrapper } from "./StyledEditProfileComponents" import { TestimoniesTab } from "./TestimoniesTab" +import { useFlags } from "components/featureFlags" +import { PendingUpgradeBanner } from "components/PendingUpgradeBanner" +import { TabContext } from "components/shared/ProfileTabsContext" export function EditProfile() { const { user } = useAuth() @@ -62,11 +63,11 @@ export function EditProfileForm({ notificationFrequency: notificationFrequency }: Profile = profile - const [key, setKey] = useState("AboutYou") + const { tabStatus, setTabStatus } = useContext(TabContext) const [formUpdated, setFormUpdated] = useState(false) const [settingsModal, setSettingsModal] = useState<"show" | null>(null) const [notifications, setNotifications] = useState< - "Daily" | "Weekly" | "Monthly" | "None" + "Weekly" | "Monthly" | "None" >(notificationFrequency ? notificationFrequency : "Monthly") const [isProfilePublic, setIsProfilePublic] = useState( isPublic ? isPublic : false @@ -146,8 +147,8 @@ export function EditProfileForm({ /> setKey(k)} + activeKey={tabStatus} + onSelect={(k: any) => setTabStatus(k)} > {tabs.map((t, i) => ( diff --git a/components/EditProfilePage/FollowingTab.tsx b/components/EditProfilePage/FollowingTab.tsx index 6577488d3..4eba063d5 100644 --- a/components/EditProfilePage/FollowingTab.tsx +++ b/components/EditProfilePage/FollowingTab.tsx @@ -1,5 +1,5 @@ import { collection, getDocs, query, where } from "firebase/firestore" -import { getFunctions, httpsCallable } from "firebase/functions" +import { getFunctions } from "firebase/functions" import { useTranslation } from "next-i18next" import { useCallback, useEffect, useMemo, useState } from "react" import { useAuth } from "../auth" @@ -9,11 +9,7 @@ import { TitledSectionCard } from "../shared" import UnfollowItem, { UnfollowModalConfig } from "./UnfollowModal" import { FollowedItem } from "./FollowingTabComponents" import { BillElement, UserElement } from "./FollowingTabComponents" - -const functions = getFunctions() - -const unfollowBillFunction = httpsCallable(functions, "unfollowBill") -const unfollowUserFunction = httpsCallable(functions, "unfollowUser") +import { deleteItem } from "components/shared/FollowingQueries" export function FollowingTab({ className }: { className?: string }) { const { user } = useAuth() @@ -61,7 +57,7 @@ export function FollowingTab({ className }: { className?: string }) { const q = query( subscriptionRef, where("uid", "==", `${uid}`), - where("type", "==", "org") + where("type", "==", "testimony") ) const querySnapshot = await getDocs(q) querySnapshot.forEach(doc => { @@ -69,7 +65,7 @@ export function FollowingTab({ className }: { className?: string }) { usersList.push(doc.data().userLookup) }) - if (usersFollowing.length === 0 && usersFollowing.length != 0) { + if (usersFollowing.length === 0 && usersList.length != 0) { setUsersFollowing(usersList) } }, [subscriptionRef, uid, usersFollowing]) @@ -97,28 +93,10 @@ export function FollowingTab({ className }: { className?: string }) { if (unfollow === null) { return } - // rest of what was inside the original if statement - if (unfollow.type == "bill") { - const billLookup = { billId: unfollow.typeId, court: unfollow.court } - try { - const response = await unfollowBillFunction({ - billLookup - }) - console.log(response.data) // This should print { status: 'success', message: 'Subscription removed' } - } catch (error: any) { - console.log(error.message) - } - } else { - const userLookup = { - profileId: unfollow.typeId, - fullName: unfollow.userName - } - try { - const response = await unfollowUserFunction({ userLookup: userLookup }) - console.log(response.data) // This should print { status: 'success', message: 'Subscription removed' } - } catch (error: any) { - console.log(error.message) - } + try { + deleteItem({ uid, unfollowItem: unfollow }) + } catch (error: any) { + console.log(error.message) } setBillsFollowing([]) diff --git a/components/EditProfilePage/ProfileSettingsModal.tsx b/components/EditProfilePage/ProfileSettingsModal.tsx index 8121156dc..f721ec08f 100644 --- a/components/EditProfilePage/ProfileSettingsModal.tsx +++ b/components/EditProfilePage/ProfileSettingsModal.tsx @@ -15,9 +15,7 @@ type Props = Pick & { role: Role setIsProfilePublic: Dispatch> notifications: Frequency - setNotifications: Dispatch< - SetStateAction<"Daily" | "Weekly" | "Monthly" | "None"> - > + setNotifications: Dispatch> onSettingsModalClose: () => void } @@ -100,8 +98,16 @@ export default function ProfileSettingsModal({ aria-labelledby="notifications-modal" centered > - + {t("setting")} + {t("navigation.closeNavMenu")}
@@ -140,9 +146,6 @@ export default function ProfileSettingsModal({ variant="outline-secondary" /> - setNotifications("Daily")}> - {t("email.daily")} - setNotifications("Weekly")}> {t("email.weekly")} diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx index be24bd7d5..7212b2f1e 100644 --- a/components/Footer/Footer.tsx +++ b/components/Footer/Footer.tsx @@ -154,6 +154,9 @@ const AccountLinks = ({ authenticated, user, signOut }: PageFooterProps) => { > {t("navigation.accountProfile")} + + {t("navigation.newsfeed")} + signOut()}> {t("signOut", { ns: "auth" })} diff --git a/components/HearingsScheduled/HearingsScheduled.tsx b/components/HearingsScheduled/HearingsScheduled.tsx index d776b2b74..723c4ff48 100644 --- a/components/HearingsScheduled/HearingsScheduled.tsx +++ b/components/HearingsScheduled/HearingsScheduled.tsx @@ -3,6 +3,7 @@ import { Container, Carousel, Spinner } from "react-bootstrap" import styled from "styled-components" import { Col, Row } from "../bootstrap" import { useCalendarEvents } from "./calendarEvents" +import { useTranslation } from "react-i18next" export type EventData = { index: number @@ -175,6 +176,7 @@ const CarouselControlPrevIcon = styled.span` export const HearingsScheduled = () => { const [monthIndex, setMonthIndex] = useState(0) + const { t } = useTranslation("homepage") const handleSelect = ( selectedIndex: number, @@ -195,7 +197,7 @@ export const HearingsScheduled = () => {
- Hearings Scheduled + {t("hearingsScheduled.title")}
@@ -264,7 +266,7 @@ export const HearingsScheduled = () => { ) : (
-

No Scheduled Events

+

{t("hearingsScheduled.noEvents")}

)} diff --git a/components/Legislative/Legislative.tsx b/components/Legislative/Legislative.tsx index cb0992d4c..c808d885a 100644 --- a/components/Legislative/Legislative.tsx +++ b/components/Legislative/Legislative.tsx @@ -1,85 +1,57 @@ import { Container } from "react-bootstrap" import { TestimonyCardList } from "components/LearnTestimonyComponents/TestimonyCardComponents" +import { useTranslation } from "react-i18next" const LegislativeContent = [ { - title: "Filing", - paragraphs: [ - `A Representative, Senator, or the Governor must file a bill. Constituents can write to their legislators (typically their own district's House or Senate member) to propose a bill to be filed. This can be done at any time, but is typically done before the start of the legislative session (i.e. before the third Friday in January of odd-numbered years). Bills filed later, during the legislative session, require special approval.` - ], src: "speaker-podium.svg", alt: "" }, { - title: "Testimony", - paragraphs: [ - `All stakeholders have the chance to share their thoughts with the legislature by submitting written testimony. Typically, testimony is submitted to the legislative Committee responsible for the bill in advance of their public hearing. You can also deliver oral testimony by attending a hearing or you can reach out to your legislators and speak to them directly.` - ], src: "mic-with-testify.svg", alt: "" }, { - title: "Public hearing", - paragraphs: [ - `All bills are formally heard by committees during public hearings which are open to the public and recorded and posted as videos on the legislature's website. While the amount of time available to speak during a public hearing is limited, more detailed comments can be submitted to the committee in written testimony.` - ], src: "doc-with-arrows-to-people.svg", alt: "" }, { - title: "Committee reports", - paragraphs: [ - `Committee reports. Committees must file reports on each bill under their consideration after discussing them in Executive Session following the public hearing. This typically occurs before February of the second year of the legislative session (even-numbered years). The goal for proponents of a bill is that the Committee will recommend they "ought to pass" and thereby promote them out of the Committee. Most bills, however are "sent to study," meaning that they will not be passed out of committee in that session. A successful bill may be redrafted or amended by the committee based on testimony received and other deliberations. Many of those bills will be refiled in the next session and can be considered again.` - ], src: "leg-with-lightbulb.svg", alt: "" }, { - title: "Three readings", - paragraphs: [ - `Each bill passed out of Committee will be "read" three times by each branch, the House and Senate. This typically entails floor debate of the full chamber and a vote of the Committee on Ways and Means or Steering and Policy.` - ], src: "speaker-with-leg.svg", alt: "" }, { - title: "Engrossment and Enactment", - paragraphs: [ - `After the three readings, the bill will be voted on by each of the full chambers (House and Senate), resulting in (if successful) "engrossment" and then "enactment.` - ], src: "opinions.svg", alt: "" }, { - title: "Conference Committee", - paragraphs: [ - `If necessary, differences between the House and Senate versions of a bill will be reconciled by a temporary Conference Committee appointed by the House Speaker and Senate President.` - ], src: "respect-with-blob.svg", alt: "" }, { - title: "Executive branch", - paragraphs: [ - `Lastly, the Governor is responsible for signing the enacted and reconciled bill into law. The governor can also veto the bill, return it to the Legislature for changes, or take a number of other less-common actions.` - ], src: "speaker-with-pen.svg", alt: "" } ] const Legislative = () => { + const { t } = useTranslation("learnComponents") + return (

- Understanding the Massachusetts Legislative Process + {t("legislative.title")}

-

- Some of the key steps in the legislative process for how most bills - become laws in MA. -

+

{t("legislative.intro")}

({ + ...value, + title: t(`legislative.content.${index}.title`), + paragraphs: [t(`legislative.content.${index}.paragraph`)] + }))} shouldAlternateImages={false} />
diff --git a/components/MobileNav.tsx b/components/Navbar.tsx similarity index 51% rename from components/MobileNav.tsx rename to components/Navbar.tsx index 68b106519..99372dad1 100644 --- a/components/MobileNav.tsx +++ b/components/Navbar.tsx @@ -1,9 +1,12 @@ import { useTranslation } from "next-i18next" -import React, { useState } from "react" +import React, { useContext, useState } from "react" import Image from "react-bootstrap/Image" import styled from "styled-components" +import { useMediaQuery } from "usehooks-ts" import { SignInWithButton, signOutAndRedirectToHome, useAuth } from "./auth" -import { Col, Nav, Navbar, NavDropdown } from "./bootstrap" +import { Col, Container, Dropdown, Nav, Navbar, NavDropdown } from "./bootstrap" +import { TabContext } from "./shared/ProfileTabsContext" + import { Avatar, NavbarLinkAI, @@ -13,6 +16,7 @@ import { NavbarLinkFAQ, NavbarLinkGoals, NavbarLinkLogo, + NavbarLinkNewsfeed, NavbarLinkProcess, NavbarLinkSignOut, NavbarLinkSupport, @@ -22,7 +26,13 @@ import { NavbarLinkWhyUse } from "./NavbarComponents" -export const MobileNav: React.FC> = () => { +export const MainNavbar: React.FC> = () => { + const isMobile = useMediaQuery("(max-width: 768px)") + + return <>{isMobile ? : } +} + +const MobileNav: React.FC> = () => { const BlackCollapse = styled(() => { return ( @@ -40,14 +50,25 @@ export const MobileNav: React.FC> = () => { } ` + const { tabStatus, setTabStatus } = useContext(TabContext) + const ProfileLinks = () => { return (