diff --git a/.gitignore b/.gitignore index 3151a5f59..f06e138b2 100644 --- a/.gitignore +++ b/.gitignore @@ -80,4 +80,9 @@ build-storybook.log /token -cert.txt \ No newline at end of file +cert.txt + +# vscode + +# lets each user define their own vscode settings +.vscode/settings.json diff --git a/.vscode/launch.json b/.vscode/launch.json index 165043d3a..631085d5a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,24 +1,30 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + // adapted from https://nextjs.org/docs/app/guides/debugging#debugging-with-vs-code "version": "0.2.0", "configurations": [ { - "name": "Launch Program", - "program": "${workspaceFolder}/app.js", + "name": "Run and Debug MAPLE", + "type": "node", "request": "launch", + "runtimeExecutable": "yarn", + "runtimeArgs": [ + "dev" + ], + "env": { + "NODE_OPTIONS": "--inspect" + }, + "console": "integratedTerminal", "skipFiles": [ "/**" ], - "type": "node" - }, - { - "name": "Launch in VSCode (with TypeScript)", - "type": "node", - "request": "launch", - "program": "/Users/mrigank/maple/functions/src/notifications/index.ts", // Replace with path to your main entry point (e.g., "index.js") - "sourceMaps": true + "sourceMaps": true, + "serverReadyAction": { + "action": "debugWithChrome", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index c87b85ede..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files.exclude": { - "amplify/.config": true, - "amplify/**/*-parameters.json": true, - "amplify/**/amplify.state": true, - "amplify/**/transform.conf.json": true, - "amplify/#current-cloud-backend": true, - "amplify/backend/amplify-meta.json": true, - "amplify/backend/awscloudformation": true - } -} \ No newline at end of file diff --git a/components/Card/CardTitle.tsx b/components/Card/CardTitle.tsx index 1d97d23bb..eae1d18c1 100644 --- a/components/Card/CardTitle.tsx +++ b/components/Card/CardTitle.tsx @@ -17,6 +17,7 @@ interface CardTitleProps { isUserMatch?: boolean type?: string userRole?: string + isNewsfeed?: string } export const CardTitle = (props: CardTitleProps) => { @@ -29,7 +30,8 @@ export const CardTitle = (props: CardTitleProps) => { isUserMatch, subheader, type, - userRole + userRole, + isNewsfeed } = props return ( @@ -44,14 +46,18 @@ export const CardTitle = (props: CardTitleProps) => { subheader={subheader} type={type} /> - + {isNewsfeed ? ( + + ) : ( + <> + )} ) diff --git a/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx b/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx index 82aa7b066..cc667c3d4 100644 --- a/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx +++ b/components/CommunicatingWithLegislators/CommunicatingWithLegislatorsContent.tsx @@ -1,6 +1,7 @@ import { Row, Col } from "../bootstrap" import Image from "react-bootstrap/Image" import styled from "styled-components" +import { useTranslation } from "next-i18next" const StyledImage = styled(Image)` width: 12.5rem; @@ -27,9 +28,9 @@ const WritingContent = () => (

- You can submit your thoughts on a bill to the Committee hearing it - before the date of their public hearing. This website, the MAPLE - platform, focuses on this mechanism. + {useTranslation("learnComponents").t( + "communicating.testifyInWriting.content" + )}

@@ -58,8 +59,9 @@ const OralContent = () => ( lg={{ span: 7, order: 1 }} >

- You can attend a public hearing for a bill of interest to you and sign - up for a slot to speak before the Committee. + {useTranslation("learnComponents").t( + "communicating.testifyOrally.content" + )}

@@ -69,12 +71,9 @@ const WriteOrCallContent = () => (

- You can contact your legislators any time by looking up their contact - information on the MA Legislature website. Your voice will probably - carry the most weight with the House and Senate representatives of your - own district, but you are free to contact Committee Chairs or any other - member of the legislature with your opinions. You could request a - meeting in person. + {useTranslation("learnComponents").t( + "communicating.writeOrCall.content" + )}

diff --git a/components/EditProfilePage/ProfileSettingsModal.tsx b/components/EditProfilePage/ProfileSettingsModal.tsx index f721ec08f..59d47968e 100644 --- a/components/EditProfilePage/ProfileSettingsModal.tsx +++ b/components/EditProfilePage/ProfileSettingsModal.tsx @@ -82,7 +82,7 @@ export default function ProfileSettingsModal({ const handleToggleNotifications = async () => { if (notifications === "None") { - setNotifications("Monthly") + setNotifications("Weekly") } else { setNotifications("None") } diff --git a/components/Footer/Footer.tsx b/components/Footer/Footer.tsx index 911a9451e..cb72c8b48 100644 --- a/components/Footer/Footer.tsx +++ b/components/Footer/Footer.tsx @@ -11,6 +11,7 @@ import CustomDropdown, { CustomDropdownProps } from "components/Footer/CustomFooterDropdown" import { FooterContainer } from "./FooterContainer" +import { NEWSLETTER_SIGNUP_URL } from "components/common" export type PageFooterProps = { children?: any @@ -282,7 +283,7 @@ const PageFooter = (props: PageFooterProps) => { {t("legal.disclaimer")} {" - "} { const { t } = useTranslation("common") @@ -59,7 +60,7 @@ const HeroHeader = ({ authenticated }) => {

{t("newcomer")}{" "} {

{

{t("communicating.intro")}

- + - + - + diff --git a/components/Newsfeed/Newsfeed.tsx b/components/Newsfeed/Newsfeed.tsx index 4b9904d89..f05dc6ac5 100644 --- a/components/Newsfeed/Newsfeed.tsx +++ b/components/Newsfeed/Newsfeed.tsx @@ -144,16 +144,14 @@ export default function Newsfeed() { const [settingsModal, setSettingsModal] = useState<"show" | null>(null) const [notifications, setNotifications] = useState< "Weekly" | "Monthly" | "None" - >(notificationFrequency ? notificationFrequency : "Monthly") + >(notificationFrequency ? notificationFrequency : "Weekly") const [isProfilePublic, setIsProfilePublic] = useState( isPublic ? isPublic : false ) const onSettingsModalOpen = () => { setSettingsModal("show") - setNotifications( - notificationFrequency ? notificationFrequency : "Monthly" - ) + setNotifications(notificationFrequency ? notificationFrequency : "Weekly") setIsProfilePublic(isPublic ? isPublic : false) } @@ -230,6 +228,7 @@ export default function Newsfeed() { testimonyId={element.testimonyId} type={element.type} userRole={element.userRole} + isNewsfeed={"enable newsfeed specific subheading"} /> ))} diff --git a/components/NewsfeedCard/NewsfeedCard.tsx b/components/NewsfeedCard/NewsfeedCard.tsx index c955e6993..2aaaded6b 100644 --- a/components/NewsfeedCard/NewsfeedCard.tsx +++ b/components/NewsfeedCard/NewsfeedCard.tsx @@ -25,6 +25,7 @@ export const NewsfeedCard = (props: { timestamp: Timestamp type: string userRole?: string + isNewsfeed?: string }) => { const date = props.timestamp.toDate() const formattedTimestamp = `${date.toLocaleDateString()}` @@ -40,6 +41,7 @@ export const NewsfeedCard = (props: { timestamp={formattedTimestamp} type={props.type} userRole={props.userRole} + isNewsfeed={"enable newsfeed specific subheading"} /> ) diff --git a/components/OurTeam/OurTeam.tsx b/components/OurTeam/OurTeam.tsx index 668a4a485..4d7d13f28 100644 --- a/components/OurTeam/OurTeam.tsx +++ b/components/OurTeam/OurTeam.tsx @@ -8,6 +8,7 @@ import { SteeringCommittee } from "./SteeringCommittee" import { AdvisoryBoard } from "./AdvisoryBoard" import { OurPartners } from "./Partners" import { useMediaQuery } from "usehooks-ts" +import { useTranslation } from "next-i18next" export const OurTeam = () => { const isMobile = useMediaQuery("(max-width: 768px)") @@ -36,13 +37,14 @@ export const OurTeam = () => { } const TabGroup = () => { + const { t } = useTranslation("our-team") return ( @@ -50,14 +52,14 @@ const TabGroup = () => { @@ -66,6 +68,7 @@ const TabGroup = () => { } const TabDropdown = () => { + const { t } = useTranslation("our-team") const [selectedTab, setSelectedTab] = useState("Steering Committee") const handleTabClick = (tabTitle: string) => { @@ -85,7 +88,7 @@ const TabDropdown = () => { eventKey="steering_committee" onClick={() => handleTabClick("Steering Committee")} > - Steering Committee + {t("steering.title")} { eventKey="advisory_board" onClick={() => handleTabClick("Advisory Board")} > - Advisory Board + {t("advisory.title")} { eventKey="partners" onClick={() => handleTabClick("Partners")} > - Partners + {t("partners.title")} diff --git a/components/auth/TermsOfServiceModal.tsx b/components/auth/TermsOfServiceModal.tsx index a2b752acc..3c2fc7c93 100644 --- a/components/auth/TermsOfServiceModal.tsx +++ b/components/auth/TermsOfServiceModal.tsx @@ -3,6 +3,7 @@ import { Button, Col, Modal, Row, Stack } from "../bootstrap" import { External } from "components/links" import { NavLink } from "../Navlink" import { useTranslation } from "next-i18next" +import { NEWSLETTER_SIGNUP_URL } from "components/common" export default function TermsOfServiceModal({ show, @@ -80,7 +81,7 @@ export default function TermsOfServiceModal({ > = ({ return
{children}
} +/** + * Props for the Layout component. + * + * @property {string} [titleI18nKey] - The internationalization key for the title. + * This key must be defined in the `common.json` namespace. + */ export type LayoutProps = { - title?: string + titleI18nKey?: string } export const Layout: React.FC> = ({ children, - title + titleI18nKey }) => { const { authenticated, user } = useAuth() const { t } = useTranslation("common") - const formattedTitle = title - ? `${title} | ${t("maple_abbr")}: ${t("maple_fullName")}` + let title = titleI18nKey + ? `${t(titleI18nKey)} | ${t("maple_abbr")}: ${t("maple_fullName")}` : `${t("maple_abbr")}: ${t("maple_fullName")}` // isClient used to prevent hydration issues: quite possibly better solutions exist @@ -41,7 +47,7 @@ export const Layout: React.FC> = ({ {isClient ? ( <> - {formattedTitle} + {title} diff --git a/components/page.tsx b/components/page.tsx index a40548c67..ebdea435c 100644 --- a/components/page.tsx +++ b/components/page.tsx @@ -13,17 +13,15 @@ export type AppPropsWithLayout = AppProps & { export function applyLayout({ Component, pageProps }: AppPropsWithLayout) { const page = - return {page} + return {page} } -export type PageOptions

= { - title?: string - fullWidth?: boolean - Page: NextPage

-} - -export function createPage

(options: PageOptions

): AppPage

{ +export function createPage

( + options: { + Page: NextPage

+ } & LayoutProps +): AppPage

{ const page: AppPage

= options.Page - page.title = options.title + page.titleI18nKey = options.titleI18nKey return page } diff --git a/components/search/bills/BillSearch.tsx b/components/search/bills/BillSearch.tsx index faadd018e..80cefa3ec 100644 --- a/components/search/bills/BillSearch.tsx +++ b/components/search/bills/BillSearch.tsx @@ -7,21 +7,23 @@ import { SearchBox, useInstantSearch } from "react-instantsearch" -import { currentGeneralCourt } from "functions/src/shared" +import { createInstantSearchRouterNext } from "react-instantsearch-router-nextjs" +import singletonRouter from "next/router" import styled from "styled-components" import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter" +import { currentGeneralCourt } from "functions/src/shared" import { Col, Container, Row, Spinner } from "../../bootstrap" import { NoResults } from "../NoResults" import { ResultCount } from "../ResultCount" import { SearchContainer } from "../SearchContainer" import { SearchErrorBoundary } from "../SearchErrorBoundary" -import { useRouting } from "../useRouting" import { BillHit } from "./BillHit" import { useBillRefinements } from "./useBillRefinements" import { SortBy, SortByWithConfigurationItem } from "../SortBy" import { getServerConfig, VirtualFilters } from "../common" import { useBillSort } from "./useBillSort" import { FC, useState } from "react" +import { pathToSearchState, searchStateToUrl } from "../routingHelpers" const searchClient = new TypesenseInstantSearchAdapter({ server: getServerConfig(), @@ -70,8 +72,16 @@ export const BillSearch = () => { } }} searchClient={searchClient} - routing={useRouting()} - future={{ preserveSharedStateOnUnmount: true }} + routing={{ + router: createInstantSearchRouterNext({ + singletonRouter, + routerOptions: { + cleanUrlOnDispose: false, + createURL: args => searchStateToUrl(args), + parseURL: args => pathToSearchState(args) + } + }) + }} > diff --git a/components/search/routingHelpers.ts b/components/search/routingHelpers.ts new file mode 100644 index 000000000..42e139b30 --- /dev/null +++ b/components/search/routingHelpers.ts @@ -0,0 +1,36 @@ +import { UiState } from "instantsearch.js" +import QueryString from "qs" + +export const searchStateToUrl = (createUrlArgs: { + location: Location + qsModule: typeof QueryString + routeState: UiState +}) => { + const { location, qsModule: qs, routeState: searchState } = createUrlArgs + const base = location.origin + location.pathname + + const flagQueries = Object.fromEntries( + Object.entries(qs.parse(window.location.search.slice(1))).filter( + ([key]) => !Object.keys(searchState).includes(key) + ) + ) + + const query = qs.stringify({ + ...searchState, + ...flagQueries + }) + + return query ? `${base}?${query}` : base +} + +export const pathToSearchState = (parseUrlArgs: { + location: Location + qsModule: typeof QueryString +}) => { + const { location, qsModule: qs } = parseUrlArgs + const path = location.href + + return ( + path.includes("?") ? qs.parse(path.substring(path.indexOf("?") + 1)) : {} + ) as UiState +} diff --git a/components/search/testimony/TestimonySearch.tsx b/components/search/testimony/TestimonySearch.tsx index e77b42645..f9fe19271 100644 --- a/components/search/testimony/TestimonySearch.tsx +++ b/components/search/testimony/TestimonySearch.tsx @@ -6,6 +6,8 @@ import { SearchBox, useInstantSearch } from "react-instantsearch" +import { createInstantSearchRouterNext } from "react-instantsearch-router-nextjs" +import singletonRouter from "next/router" import { StyledTabContent, StyledTabNav @@ -23,10 +25,10 @@ import { SearchContainer } from "../SearchContainer" import { SearchErrorBoundary } from "../SearchErrorBoundary" import { SortBy } from "../SortBy" import { getServerConfig, VirtualFilters } from "../common" -import { useRouting } from "../useRouting" import { TestimonyHit } from "./TestimonyHit" import { useTestimonyRefinements } from "./useTestimonyRefinements" import { FollowContext, OrgFollowStatus } from "components/shared/FollowContext" +import { pathToSearchState, searchStateToUrl } from "../routingHelpers" const searchClient = new TypesenseInstantSearchAdapter({ server: getServerConfig(), @@ -63,8 +65,16 @@ export const TestimonySearch = () => ( } }} searchClient={searchClient} - routing={useRouting()} - future={{ preserveSharedStateOnUnmount: true }} + routing={{ + router: createInstantSearchRouterNext({ + singletonRouter, + routerOptions: { + cleanUrlOnDispose: false, + createURL: args => searchStateToUrl(args), + parseURL: args => pathToSearchState(args) + } + }) + }} > diff --git a/components/search/useRouting.tsx b/components/search/useRouting.tsx deleted file mode 100644 index e4ef3b9fc..000000000 --- a/components/search/useRouting.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { UiState } from "instantsearch.js" -import { RouterProps } from "instantsearch.js/es/middlewares" -import Router from "next/router" -import qs from "qs" -import { useMemo } from "react" - -const pathToSearchState = (path: string) => - (path.includes("?") - ? qs.parse(path.substring(path.indexOf("?") + 1)) - : {}) as UiState - -const searchStateToUrl = (searchState: UiState) => { - const base = window.location.pathname - - const flagQueries = Object.fromEntries( - Object.entries(qs.parse(window.location.search.slice(1))).filter( - ([key]) => !Object.keys(searchState).includes(key) - ) - ) - - const query = qs.stringify({ - ...searchState, - ...flagQueries - }) - - return query ? `${base}?${query}` : base -} - -export function useRouting(): RouterProps { - return useMemo(() => { - let disposed = false - return { - router: { - createURL: searchStateToUrl, - dispose() { - disposed = true - // Clear back listener - Router.beforePopState(() => true) - }, - onUpdate(callback) { - Router.beforePopState(({ url }) => { - callback(pathToSearchState(url)) - return true - }) - }, - read: () => pathToSearchState(window.location.href), - write(route) { - if (disposed) return - const url = searchStateToUrl(route) - Router.push(url, url, { shallow: true }) - } - } - } - }, []) -} diff --git a/functions/src/notifications/deliverNotifications.ts b/functions/src/notifications/deliverNotifications.ts index aebf51290..3f39c5a91 100644 --- a/functions/src/notifications/deliverNotifications.ts +++ b/functions/src/notifications/deliverNotifications.ts @@ -66,7 +66,7 @@ const deliverEmailNotifications = async () => { } // Temporarily using email from the profile to test the non-auth issues - const verifiedEmail = profile.email //await getVerifiedUserEmail(profileDoc.id) + const verifiedEmail = profile.email || profile.contactInfo?.publicEmail //await getVerifiedUserEmail(profileDoc.id) if (!verifiedEmail) { console.log( `Skipping user ${profileDoc.id} because they have no verified email address` diff --git a/functions/src/notifications/types.ts b/functions/src/notifications/types.ts index 3681b53e0..b25278ec3 100644 --- a/functions/src/notifications/types.ts +++ b/functions/src/notifications/types.ts @@ -68,4 +68,7 @@ export interface Profile { email?: string notificationFrequency?: Frequency nextDigestAt?: FirebaseFirestore.Timestamp + contactInfo?: { + publicEmail?: string + } } diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..a4a7b3f5c 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. diff --git a/package.json b/package.json index 1d006efd6..69d09debe 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "react-i18next": "^13.2.2", "react-inlinesvg": "^3.0.1", "react-instantsearch": "^7.12.4", + "react-instantsearch-router-nextjs": "^7.15.5", "react-is": "^18.2.0", "react-markdown": "^8.0.4", "react-overlays": "^5.1.1", diff --git a/pages/about/faq-page.tsx b/pages/about/faq-page.tsx index cbcd42c7b..e4aea8e8e 100644 --- a/pages/about/faq-page.tsx +++ b/pages/about/faq-page.tsx @@ -3,7 +3,7 @@ import { FaqPage } from "../../components/Faq/FaqPage" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "About", + titleI18nKey: "about", Page: () => { return } diff --git a/pages/about/how-maple-uses-ai.tsx b/pages/about/how-maple-uses-ai.tsx index 17728c15e..82a8d8934 100644 --- a/pages/about/how-maple-uses-ai.tsx +++ b/pages/about/how-maple-uses-ai.tsx @@ -3,7 +3,7 @@ import { createGetStaticTranslationProps } from "components/translations" import MapleAI from "components/about/MapleAI/MapleAI" export default createPage({ - title: "How Maple Uses AI", + titleI18nKey: "navigation.ai", Page: () => { return (

diff --git a/pages/about/mission-and-goals.tsx b/pages/about/mission-and-goals.tsx index 216f191d6..7119d8018 100644 --- a/pages/about/mission-and-goals.tsx +++ b/pages/about/mission-and-goals.tsx @@ -3,7 +3,7 @@ import GoalsAndMission from "../../components/GoalsAndMission/GoalsAndMission" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "About", + titleI18nKey: "about", Page: () => { return } diff --git a/pages/about/our-team.tsx b/pages/about/our-team.tsx index 3454050c6..18b2b5731 100644 --- a/pages/about/our-team.tsx +++ b/pages/about/our-team.tsx @@ -3,7 +3,7 @@ import { createGetStaticTranslationProps } from "components/translations" import { OurTeam } from "../../components/OurTeam/OurTeam" export default createPage({ - title: "Our Team", + titleI18nKey: "titles.our_team", Page: () => { return (
diff --git a/pages/about/support-maple.tsx b/pages/about/support-maple.tsx index 069ac2226..dfdb31de9 100644 --- a/pages/about/support-maple.tsx +++ b/pages/about/support-maple.tsx @@ -3,7 +3,7 @@ import SupportMaple from "../../components/about/SupportMaple/SupportMaple" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "How to Support MAPLE", + titleI18nKey: "titles.support_maple", Page: () => { return (
diff --git a/pages/admin.tsx b/pages/admin.tsx index 2f81654b6..4aee1bf15 100644 --- a/pages/admin.tsx +++ b/pages/admin.tsx @@ -8,7 +8,7 @@ import { useEffect } from "react" // return export default createPage({ - title: "Admin", + titleI18nKey: "titles.admin", Page: requireAdmin(() => ) }) diff --git a/pages/bills/[court]/[billId].tsx b/pages/bills/[court]/[billId].tsx index 288207655..b60308a7d 100644 --- a/pages/bills/[court]/[billId].tsx +++ b/pages/bills/[court]/[billId].tsx @@ -10,7 +10,7 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations" const Query = z.object({ court: z.coerce.number(), billId: z.string({}) }) export default createPage<{ bill: Bill }>({ - title: "Bill", + titleI18nKey: "titles.bill", Page: ({ bill }) => { return ( <> diff --git a/pages/bills/index.tsx b/pages/bills/index.tsx index 52c96ae64..b44a18305 100644 --- a/pages/bills/index.tsx +++ b/pages/bills/index.tsx @@ -5,7 +5,7 @@ import { BillSearch } from "components/search" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "Browse Bills", + titleI18nKey: "navigation.browseBills", Page: () => { const { t } = useTranslation("billSearch") diff --git a/pages/edit-profile/[[...docName]].tsx b/pages/edit-profile/[[...docName]].tsx index 68afe0970..0fa1207e3 100644 --- a/pages/edit-profile/[[...docName]].tsx +++ b/pages/edit-profile/[[...docName]].tsx @@ -13,7 +13,7 @@ const Query = z.object({ }) export default createPage({ - title: "Edit Profile", + titleI18nKey: "navigation.editProfile", Page: () => { // Page: requireAuth(({ user }) => { const tabTitle = Query.parse(useRouter().query).docName?.[0] || "about-you" diff --git a/pages/learn/[slug].tsx b/pages/learn/[slug].tsx index 423a64b86..491619fb4 100644 --- a/pages/learn/[slug].tsx +++ b/pages/learn/[slug].tsx @@ -54,7 +54,7 @@ const tabs: TabsType[] = [ ] export default createPage<{ params: IParams }>({ - title: "Learn", + titleI18nKey: "learn", Page: props => { const router = useRouter() diff --git a/pages/learn/legislative-process.tsx b/pages/learn/legislative-process.tsx index bf637c6f4..76c892b74 100644 --- a/pages/learn/legislative-process.tsx +++ b/pages/learn/legislative-process.tsx @@ -6,7 +6,7 @@ import { createGetStaticTranslationProps } from "components/translations" // TODO: Change export default createPage({ - title: "How To Have Impact Through Legislative Testimony", + titleI18nKey: "titles.legislative_process", Page: () => { return ( diff --git a/pages/newsfeed.tsx b/pages/newsfeed.tsx index f68c104da..7c409a4cd 100644 --- a/pages/newsfeed.tsx +++ b/pages/newsfeed.tsx @@ -2,7 +2,7 @@ import { createPage } from "../components/page" import Newsfeed from "components/Newsfeed/Newsfeed" export default createPage({ - title: "Newsfeed", + titleI18nKey: "navigation.newsfeed", Page: () => { return } diff --git a/pages/organizations/[orgid].tsx b/pages/organizations/[orgid].tsx index 55438f459..b4c5aab22 100644 --- a/pages/organizations/[orgid].tsx +++ b/pages/organizations/[orgid].tsx @@ -6,7 +6,7 @@ import { ProfilePage } from "components/ProfilePage" import { createPage } from "../../components/page" export default createPage({ - title: "Organizations", + titleI18nKey: "orgs", Page: () => { const id = useOrgRouting() diff --git a/pages/policies/[...docName].tsx b/pages/policies/[...docName].tsx index 5300fc10c..88bef9165 100644 --- a/pages/policies/[...docName].tsx +++ b/pages/policies/[...docName].tsx @@ -11,7 +11,7 @@ const Query = z.object({ }) export default createPage({ - title: "Terms of Service", + titleI18nKey: "titles.policies", Page: () => { const policy = Query.parse(useRouter().query).docName?.[0] || "privacy-policy" diff --git a/pages/profile.tsx b/pages/profile.tsx index 00db3a44e..1204be38a 100644 --- a/pages/profile.tsx +++ b/pages/profile.tsx @@ -7,7 +7,7 @@ import { ProfilePage } from "../components/ProfilePage" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "Profile", + titleI18nKey: "navigation.accountProfile", Page: () => { const { id, loading } = useProfileRouting() const { authenticated, user } = useAuth() diff --git a/pages/submit-testimony.tsx b/pages/submit-testimony.tsx index c6932baf9..bb46782cf 100644 --- a/pages/submit-testimony.tsx +++ b/pages/submit-testimony.tsx @@ -8,7 +8,7 @@ import { SubmitTestimonyForm } from "../components/publish/SubmitTestimonyForm" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "Submit Testimony", + titleI18nKey: "titles.submit_testimony", Page: requireAuth(() => { useSyncRouterAndStore() diff --git a/pages/testimony/[...testimonyDetail].tsx b/pages/testimony/[...testimonyDetail].tsx index c83f64de8..0d621db98 100644 --- a/pages/testimony/[...testimonyDetail].tsx +++ b/pages/testimony/[...testimonyDetail].tsx @@ -15,7 +15,7 @@ import { z } from "zod" import { createPage } from "../../components/page" export default createPage({ - title: "Testimony", + titleI18nKey: "titles.testimony", Page: () => { useSyncQueryParameters() return diff --git a/pages/testimony/index.tsx b/pages/testimony/index.tsx index 18b2e56b0..a782d9963 100644 --- a/pages/testimony/index.tsx +++ b/pages/testimony/index.tsx @@ -2,13 +2,16 @@ import { Container } from "components/bootstrap" import { createPage } from "components/page" import { TestimonySearch } from "components/search/testimony/TestimonySearch" import { createGetStaticTranslationProps } from "components/translations" +import { useTranslation } from "next-i18next" export default createPage({ - title: "Browse Testimony", + titleI18nKey: "navigation.browseTestimony", Page: () => { return ( -

Browse Testimony

+

+ {useTranslation("common").t("navigation.browseTestimony")} +

) diff --git a/pages/unsubscribe.tsx b/pages/unsubscribe.tsx index 7a39eb5c7..278629dd7 100644 --- a/pages/unsubscribe.tsx +++ b/pages/unsubscribe.tsx @@ -3,7 +3,7 @@ import { UnsubscribeConfirm } from "../components/Email/unsubscribe" import { createGetStaticTranslationProps } from "components/translations" export default createPage({ - title: "Unsubscribe", + titleI18nKey: "titles.unsubscribe", Page: () => { return } diff --git a/pages/why-use-maple/[slug].tsx b/pages/why-use-maple/[slug].tsx index 0886e1151..f0a2fe36b 100644 --- a/pages/why-use-maple/[slug].tsx +++ b/pages/why-use-maple/[slug].tsx @@ -45,7 +45,7 @@ const tabs: TabsType[] = [ ] export default createPage<{ params: IParams }>({ - title: "Why Use Maple?", + titleI18nKey: "titles.why_use_maple", Page: props => { const router = useRouter() diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 84248a859..eab8fbaf4 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -127,5 +127,17 @@ "table": { "page": "Page {{currentPage}}" }, + "titles": { + "admin": "Admin", + "bill": "Bill", + "our_team": "Our Team", + "legislative_process": "How To Have Impact Through Legislative Testimony", + "submit_testimony": "Submit Testimony", + "support_maple": "How to Support MAPLE", + "testimony": "Testimony", + "policies": "Policies", + "unsubscribe": "Unsubscribe", + "why_use_maple": "Why Use Maple?" + }, "user_updates": "User Updates" } diff --git a/public/locales/en/learnComponents.json b/public/locales/en/learnComponents.json index a36609009..c4c9d44cc 100644 --- a/public/locales/en/learnComponents.json +++ b/public/locales/en/learnComponents.json @@ -83,9 +83,18 @@ "communicating": { "title": "Communicating with Legislators", "intro": "There are multiple ways to share your perspective and knowledge with your legislators.", - "testifyInWriting": "Testify in writing", - "testifyOrally": "Testify orally", - "writeOrCall": "Write or call them" + "testifyInWriting": { + "title": "Testify in writing", + "content": "You can submit your thoughts on a bill to the Committee hearing it before the date of their public hearing. This website, the MAPLE platform, focuses on this mechanism." + }, + "testifyOrally": { + "title": "Testify orally", + "content": "You can attend a public hearing for a bill of interest to you and sign up for a slot to speak before the Committee." + }, + "writeOrCall": { + "title": "Write or call them", + "content": "You can contact your legislators any time by looking up their contact information on the MA Legislature website. Your voice will probably carry the most weight with the House and Senate representatives of your own district, but you are free to contact Committee Chairs or any other member of the legislature with your opinions. You could request a meeting in person." + } }, "legislative": { "title": "Understanding the Massachusetts Legislative Process", diff --git a/public/locales/en/our-team.json b/public/locales/en/our-team.json index 39990a36d..5883a6dc7 100644 --- a/public/locales/en/our-team.json +++ b/public/locales/en/our-team.json @@ -1,6 +1,6 @@ { "advisory": { - "title" : "Advisory Board", + "title": "Advisory Board", "desc": "The Advisory Board provides strategic advice and domain expertise to the MAPLE project. We are grateful to have the support of:", "DFields": "David Fields, Ph.D., is Professor of the Practice within the Graduate School of Education at Northeastern University and Senior Fellow with The Burnes Center for Social Change. He brings over 20 years of higher education experience building, launching, and scaling industry aligned programs. At the Burnes Center, his work includes supporting AI for Impact, and its InnovateMA co-op program in partnership with the Commonwealth of Massachusetts.", "JChang": "Jerren is the inaugural full-time President & CEO of Partners in Democracy. He is the co-founder of GenUnity - an innovative nonprofit supporting the civic learning and leadership development of hundreds of grassroots residents in Massachusetts communities and now a core pillar of PID's holistic democracy renovation strategy. Before GenUnity, Jerren served in the Chicago Mayor's Office as an economic development policy staffer and worked as a consultant at McKinsey & Company advising public and social sector leaders.", @@ -27,5 +27,8 @@ "MBaggett": "Merritt Baggett is a freelance React Front-End Web Developer. He received a Bachelor's Degree in Psychology from Virginia Tech.", "KKim": "Kimin Kim is a Lead Front-end Developer at the Data Lab for Tech Impact with a Bachelor's Degree of Computer Science from Northeastern University. His hobbies are playing volleyball/badminton, eating good food, and learning new skills.", "MChai": "Minqi Chai is a social scientist and user experience researcher, advocating for human-centered approaches in digital transformation. She received her PhD in Political Science and Government from Cornell University." + }, + "partners": { + "title": "Partners" } -} \ No newline at end of file +} diff --git a/tsconfig.json b/tsconfig.json index e047c1dff..3dc15fceb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,7 @@ "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, + "sourceMap": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", diff --git a/yarn.lock b/yarn.lock index ceef2062b..c3169cf13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5291,6 +5291,13 @@ algoliasearch-helper@3.22.5: dependencies: "@algolia/events" "^4.0.1" +algoliasearch-helper@3.24.3: + version "3.24.3" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.24.3.tgz#9a358c3110bcd912e79ef606a6e7bdd7725d22ee" + integrity sha512-3QKg5lzSfUiPN8Hn1ViHEGv6PjK7i4SFEDLzwlSzPO/4mVOsyos7B7/AsEtFQW5KHHPiCq6DyJl+mzg7CYlEgw== + dependencies: + "@algolia/events" "^4.0.1" + all-contributors-cli@^6.20.5: version "6.26.1" resolved "https://registry.yarnpkg.com/all-contributors-cli/-/all-contributors-cli-6.26.1.tgz#9f3358c9b9d0a7e66c8f84ffebf5a6432a859cae" @@ -10401,6 +10408,13 @@ install-artifact-from-github@^1.3.5: resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.3.5.tgz#88c96fe40e5eb21d45586d564208c648a1dbf38d" integrity sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg== +instantsearch-ui-components@0.11.1: + version "0.11.1" + resolved "https://registry.yarnpkg.com/instantsearch-ui-components/-/instantsearch-ui-components-0.11.1.tgz#664ca03f657079946e459af72fa8d2674799c466" + integrity sha512-ZqUbJYYgObQ47J08ftXV1KNC1vdEoiD4/49qrkCdW46kRzLxLgYXJGuEuk48DQwK4aBtIoccgTyfbMGfcqNjxg== + dependencies: + "@babel/runtime" "^7.1.2" + instantsearch-ui-components@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/instantsearch-ui-components/-/instantsearch-ui-components-0.9.0.tgz#f7ae71fe623d18eff32b73071749f31826cb7b89" @@ -10431,6 +10445,24 @@ instantsearch.js@4.74.2: qs "^6.5.1 < 6.10" search-insights "^2.15.0" +instantsearch.js@4.78.1: + version "4.78.1" + resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.78.1.tgz#cee799b920ba08c7c4e5af5ba591b86a1d80af1d" + integrity sha512-nDTWQ6DUxYzBZfkSxb/QJsYMZPPU8SGlGurn9147ABvA5Eumtxmk3Qy55EBMl0VxKVltGy3axAYMRB/gKIIHkg== + dependencies: + "@algolia/events" "^4.0.1" + "@types/dom-speech-recognition" "^0.0.1" + "@types/google.maps" "^3.55.12" + "@types/hogan.js" "^3.0.0" + "@types/qs" "^6.5.3" + algoliasearch-helper "3.24.3" + hogan.js "^3.0.2" + htm "^3.0.0" + instantsearch-ui-components "0.11.1" + preact "^10.10.0" + qs "^6.5.1 < 6.10" + search-insights "^2.17.2" + instantsearch.js@^4.43.0: version "4.62.0" resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.62.0.tgz#68577f4f04866728f22441cbc7464c544678d342" @@ -14805,6 +14837,24 @@ react-instantsearch-core@7.13.2: instantsearch.js "4.74.2" use-sync-external-store "^1.0.0" +react-instantsearch-core@7.15.5: + version "7.15.5" + resolved "https://registry.yarnpkg.com/react-instantsearch-core/-/react-instantsearch-core-7.15.5.tgz#65d1edc440de8dc73d55230d13af8cbcf1724221" + integrity sha512-SFxiwwMf0f5F/8U0Y4ullvQ7bZtbYE516UOJbxaHhjV8yY0i8c22K4lrBFrYbxVRT7QAcp2wLGHiB7r/lD7eRA== + dependencies: + "@babel/runtime" "^7.1.2" + algoliasearch-helper "3.24.3" + instantsearch.js "4.78.1" + use-sync-external-store "^1.0.0" + +react-instantsearch-router-nextjs@^7.15.5: + version "7.15.5" + resolved "https://registry.yarnpkg.com/react-instantsearch-router-nextjs/-/react-instantsearch-router-nextjs-7.15.5.tgz#a8b13bc5ad9bd8c5a689d48f2714eab6bed2514f" + integrity sha512-kn325Nl6QkZlkSuOXwKUOb56QkSsKes9XQdBLythKt2oZzzAfcaXSYzYsFEyj96cMYDLkRHhHvLhdyq4C8Xezg== + dependencies: + instantsearch.js "4.78.1" + react-instantsearch-core "7.15.5" + react-instantsearch@^7.12.4: version "7.13.2" resolved "https://registry.yarnpkg.com/react-instantsearch/-/react-instantsearch-7.13.2.tgz#db84d04bd399596fb0078625bc75a6abc65e4bc6" @@ -15607,6 +15657,11 @@ search-insights@^2.15.0: resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.17.2.tgz#d13b2cabd44e15ade8f85f1c3b65c8c02138629a" integrity sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g== +search-insights@^2.17.2: + version "2.17.3" + resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.17.3.tgz#8faea5d20507bf348caba0724e5386862847b661" + integrity sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ== + search-insights@^2.6.0: version "2.11.0" resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.11.0.tgz#0512ae3b801fed5ff3a2ae82840bf20ba29d82e5"