diff --git a/components/Faq/FaqQandAButton.tsx b/components/Faq/FaqQandAButton.tsx index ce2f463c3..28f944cdc 100644 --- a/components/Faq/FaqQandAButton.tsx +++ b/components/Faq/FaqQandAButton.tsx @@ -11,6 +11,18 @@ export const FaqQandAButton = ({ question, answer }: faqQandAProps) => { useEffect(() => {}, [open]) + let supportLink = null + + { + question == "How can I support MAPLE?" + ? (supportLink = ( + + {" this page"} + + )) + : null + } + return ( <> {

{answer} + {supportLink}

diff --git a/components/Faq/faqContent.json b/components/Faq/faqContent.json index 787f15f43..da2ca0458 100755 --- a/components/Faq/faqContent.json +++ b/components/Faq/faqContent.json @@ -103,15 +103,15 @@ "qAndA": [ { "question": "Who is behind MAPLE?", - "answer": "We are a collective of open source developers, legal scholars, and policy analysts & advocates seeking to leverage digital technology to improve our capacity and ability to self-govern. The MAPLE platform is a project of the NuLawLab of Northeastern University developed with Code for Boston. We are volunteer-led and operate as a nonprofit project of the 501c3 organization OpenCollective Foundation." + "answer": "We are a collective of open source developers, legal scholars, and policy analysts & advocates seeking to leverage digital technology to improve our capacity and ability to self-govern. The MAPLE platform is a project of the NuLawLab of Northeastern University developed with Code for Boston. We are volunteer-led and operate as an initiative for Partners In Democracy - Education, a 501(c)(3) organization." }, { "question": "How can I support MAPLE?", - "answer": "There are lots of ways to support MAPLE! Please visit this page for details on how to help MAPLE grow, share feedback, volunteer with us, or even contribute financially." + "answer": "There are lots of ways to support MAPLE! For details on how to help MAPLE grow, share feedback, volunteer with us, or even contribute financially, please visit " }, { "question": "Who do I contact to learn more?", - "answer": "You can reach our volunteer leadership by email at info@mapletestimony.org." + "answer": "You can reach our volunteer leadership by email at admin@mapletestimony.org." } ] } diff --git a/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx b/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx index 1192b2b7f..09e5459dd 100644 --- a/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx +++ b/components/GoalsAndMissionCardContent/GoalsAndMissionCardContent.tsx @@ -137,9 +137,7 @@ const OurMissionCardContent = () => { -

{t("mission.callout")}

- + >
@@ -156,29 +154,6 @@ const OurMissionCardContent = () => { - - - - - - - - - - - {!authenticated && ( <> diff --git a/components/OurPartnersCardContent/OurPartnersCardContent.tsx b/components/OurPartnersCardContent/OurPartnersCardContent.tsx index 813a8833e..77a27170c 100644 --- a/components/OurPartnersCardContent/OurPartnersCardContent.tsx +++ b/components/OurPartnersCardContent/OurPartnersCardContent.tsx @@ -1,5 +1,6 @@ -import { Row, Col } from "../bootstrap" +import { useTranslation } from "next-i18next" import Image from "react-bootstrap/Image" +import { Row, Col } from "../bootstrap" const NuLawLabCardContent = () => { return ( @@ -93,22 +94,17 @@ const CodeForBostonCardContent = () => { } const OpenCollectiveContent = () => { + const { t } = useTranslation("common") + return ( <> - open_collective_logo + partners in democracy logo

- MAPLE is a fiscally sponsored initiative of the 501(c)(3), the Open - Collective Foundation (OCF). You can see a full list of our donors - and expenditures on our Open Collective webpage. You can also join - the list and make a donation through the sit. + {t("partners.pid")}

diff --git a/components/OurTeam/AdvisoryBoard.tsx b/components/OurTeam/AdvisoryBoard.tsx index 76b0f7ac5..b7949abb2 100644 --- a/components/OurTeam/AdvisoryBoard.tsx +++ b/components/OurTeam/AdvisoryBoard.tsx @@ -27,8 +27,8 @@ export const AdvisoryBoard = () => { { /> { + const { t } = useTranslation("common") + return ( - Our Partners + {t("partners.header")} - The project is developed in partnership between the NuLawLab and - scholars at{" "} + {t("partners.desc1")} + + {" "} + {t("partners.desc2")} + {" "} + {t("partners.desc3")} - Boston College Law School + {" "} + {t("partners.desc4")} {" "} - and{" "} + {t("partners.desc5")}{" "} - Harvard University's Berkman Klein Center for Internet & Society + {t("partners.desc6")} . @@ -56,7 +68,7 @@ export const OurPartners = () => { - + diff --git a/components/OurTeam/SteeringCommittee.tsx b/components/OurTeam/SteeringCommittee.tsx index b5fa5aa39..8d0542de8 100644 --- a/components/OurTeam/SteeringCommittee.tsx +++ b/components/OurTeam/SteeringCommittee.tsx @@ -53,6 +53,16 @@ export const SteeringCommittee = () => { name="Dan Jackson" descr="Dan Jackson directs Northeastern University School of Law’s NuLawLab, where he draws on his design and law backgrounds to educate the legal inventors of the future." /> + + + +
@@ -60,21 +70,31 @@ export const SteeringCommittee = () => { - User Experience Design & Engineering Leads + User Experience, Design & Engineering Leads + + + + diff --git a/components/ProfilePage/FollowButton.tsx b/components/ProfilePage/FollowButton.tsx index 80103275e..1be776db6 100644 --- a/components/ProfilePage/FollowButton.tsx +++ b/components/ProfilePage/FollowButton.tsx @@ -5,19 +5,19 @@ import { Internal } from "components/links" import { StyledImage } from "./StyledProfileComponents" import { useTranslation } from "next-i18next" import { getFunctions, httpsCallable } from "firebase/functions" -import { useAuth } from "../auth" import { useState, useEffect, useCallback } from "react" import { FillButton } from "components/buttons" +import { User } from "firebase/auth" +import { Maybe } from "components/db/common" export const FollowButton = ({ profileId, - uid + user }: { profileId?: string - uid?: string + user: Maybe }) => { const { t } = useTranslation("profile") - const { user } = useAuth() const functions = getFunctions() const followBillFunction = httpsCallable(functions, "followOrg") const unfollowBillFunction = httpsCallable(functions, "unfollowOrg") @@ -25,7 +25,7 @@ export const FollowButton = ({ const topicName = `org-${profileId}` const subscriptionRef = collection( firestore, - `/users/${uid}/activeTopicSubscriptions/` + `/users/${user?.uid}/activeTopicSubscriptions/` ) const [queryResult, setQueryResult] = useState("") @@ -42,8 +42,8 @@ export const FollowButton = ({ }, [subscriptionRef, profileId, setQueryResult]) // dependencies of orgQuery useEffect(() => { - uid ? orgQuery() : null - }, [uid, orgQuery]) // dependencies of useEffect + user?.uid ? orgQuery() : null + }, [user?.uid, orgQuery]) // dependencies of useEffect const handleFollowClick = async () => { // ensure user is not null @@ -52,9 +52,6 @@ export const FollowButton = ({ } try { - if (!uid) { - throw new Error("User not found") - } const orgLookup = { profileId: profileId, type: "org" @@ -76,9 +73,6 @@ export const FollowButton = ({ } try { - if (!uid) { - throw new Error("User not found") - } const orgLookup = { profileId: profileId, type: "org" diff --git a/components/ProfilePage/ProfileButtons.tsx b/components/ProfilePage/ProfileButtons.tsx index cd16f7d48..f95bc46c6 100644 --- a/components/ProfilePage/ProfileButtons.tsx +++ b/components/ProfilePage/ProfileButtons.tsx @@ -3,12 +3,11 @@ import { useTranslation } from "next-i18next" import { Button } from "../bootstrap" import styled from "styled-components" import { FillButton, GearButton, ToggleButton } from "components/buttons" -import { Col } from "react-bootstrap" -import { Story } from "stories/atoms/BaseButton.stories" import { Internal } from "components/links" import { useProfile, ProfileHook } from "components/db" import { FollowButton } from "./FollowButton" import { useFlags } from "components/featureFlags" +import { useAuth } from "../auth" export const StyledButton = styled(Button).attrs(props => ({ className: `col-12 d-flex align-items-center justify-content-center py-3 text-nowrap`, @@ -90,9 +89,22 @@ export function ProfileButtonsUser({ ) } -export function ProfileButtonsOrg({ isUser }: { isUser: boolean }) { +export function ProfileButtonsOrg({ + profileId, + isUser +}: { + profileId: string + isUser: boolean +}) { const { followOrg } = useFlags() + const { user } = useAuth() return ( - <>{isUser ? : followOrg ? : null} + <> + {isUser ? ( + + ) : followOrg && user ? ( + + ) : null} + ) } diff --git a/components/ProfilePage/ProfileHeader.tsx b/components/ProfilePage/ProfileHeader.tsx index 800ed72e3..1526d21dd 100644 --- a/components/ProfilePage/ProfileHeader.tsx +++ b/components/ProfilePage/ProfileHeader.tsx @@ -34,7 +34,9 @@ export const ProfileHeader = ({ {profile.fullName} - {isOrg ? : null} + {isOrg ? ( + + ) : null}
diff --git a/components/about/SupportMapleCardContent/SupportMapleCardContent.tsx b/components/about/SupportMapleCardContent/SupportMapleCardContent.tsx index b4a858b7e..2139cf9aa 100644 --- a/components/about/SupportMapleCardContent/SupportMapleCardContent.tsx +++ b/components/about/SupportMapleCardContent/SupportMapleCardContent.tsx @@ -7,13 +7,12 @@ const DonateCardContent = () => {

{`${t("donate.bodytextOne")} `} {t("donate.donorsLink")} - {` ${t("donate.bodytextTwo")}`}

) @@ -33,7 +32,9 @@ const VolunteerCardContent = () => { {t("volunteer.githubLink")} {`, ${t("volunteer.bodytextTwo")} `} - info@mapletestimony.org. + + {"admin@mapletestimony.org."} +

) diff --git a/components/search/testimony/TestimonyHit.tsx b/components/search/testimony/TestimonyHit.tsx index 1793ae006..d4ce1f06e 100644 --- a/components/search/testimony/TestimonyHit.tsx +++ b/components/search/testimony/TestimonyHit.tsx @@ -8,6 +8,7 @@ import { useBill } from "components/db/bills" import { FollowOrgButton } from "components/shared/FollowButton" import { Image } from "react-bootstrap" import { useFlags } from "components/featureFlags" +import { useAuth } from "components/auth" export const TestimonyHit = ({ hit }: { hit: Hit }) => { const url = maple.testimony({ publishedId: hit.id }) @@ -37,6 +38,7 @@ const TestimonyResult = ({ hit }: { hit: Hit }) => { ) : ( hit.fullName ) + const { user } = useAuth() const { followOrg } = useFlags() return ( @@ -65,7 +67,9 @@ const TestimonyResult = ({ hit }: { hit: Hit }) => { Written by {writtenBy} - {followOrg && isOrg && } + {followOrg && isOrg && user && ( + + )}

diff --git a/functions/src/index.ts b/functions/src/index.ts index 3ed4db170..a351822bc 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -32,7 +32,8 @@ export { } from "./testimony" export { publishNotifications, - populateNotificationEvents, + populateBillNotificationEvents, + populateOrgNotificationEvents, cleanupNotifications, deliverNotifications, httpsPublishNotifications, diff --git a/functions/src/notifications/index.ts b/functions/src/notifications/index.ts index dda66127a..bd66dec64 100644 --- a/functions/src/notifications/index.ts +++ b/functions/src/notifications/index.ts @@ -1,6 +1,7 @@ // Import the functions import { publishNotifications } from "./publishNotifications" -import { populateNotificationEvents } from "./populateNotificationEvents" +import { populateBillNotificationEvents } from "./populateBillNotificationEvents" +import { populateOrgNotificationEvents } from "./populateOrgNotificationEvents" import { cleanupNotifications } from "./cleanupNotifications" import { deliverNotifications } from "./deliverNotifications" import { httpsPublishNotifications } from "./httpsPublishNotifications" @@ -12,7 +13,8 @@ import { updateNextDigestAt } from "./updateNextDigestAt" // Export the functions export { publishNotifications, - populateNotificationEvents, + populateBillNotificationEvents, + populateOrgNotificationEvents, cleanupNotifications, deliverNotifications, httpsPublishNotifications, diff --git a/functions/src/notifications/populateNotificationEvents.ts b/functions/src/notifications/populateBillNotificationEvents.ts similarity index 76% rename from functions/src/notifications/populateNotificationEvents.ts rename to functions/src/notifications/populateBillNotificationEvents.ts index 6602ae3b4..da6bff519 100644 --- a/functions/src/notifications/populateNotificationEvents.ts +++ b/functions/src/notifications/populateBillNotificationEvents.ts @@ -7,22 +7,13 @@ import * as functions from "firebase-functions" import * as admin from "firebase-admin" import { Timestamp } from "../firebase" -import { BillHistory } from "../bills/types" +import { BillNotification } from "./types" // Get a reference to the Firestore database const db = admin.firestore() -type Notification = { - type: string - court: string - id: string - name: string - history: BillHistory - historyUpdateTime: Timestamp -} - -// Define the populateNotificationEvents function -export const populateNotificationEvents = functions.firestore +// Define the populateBillNotificationEvents function +export const populateBillNotificationEvents = functions.firestore .document("/generalCourts/{court}/bills/{billId}") .onWrite(async (snapshot, context) => { if (!snapshot.after.exists) { @@ -41,13 +32,16 @@ export const populateNotificationEvents = functions.firestore if (documentCreated) { console.log("New document created") - const newNotificationEvent: Notification = { + const newNotificationEvent: BillNotification = { type: "bill", - court: court, - id: newData?.id, - name: newData?.id, - history: newData?.history, - historyUpdateTime: Timestamp.now() + + billCourt: court, + billId: newData?.id, + billName: newData?.content.Title, + + billHistory: newData?.history, + + updateTime: Timestamp.now() } await db.collection("/notificationEvents").add(newNotificationEvent) @@ -63,7 +57,9 @@ export const populateNotificationEvents = functions.firestore const notificationEventSnapshot = await db .collection("/notificationEvents") - .where("name", "==", newData?.id) + .where("type", "==", "bill") + .where("billCourt", "==", court) + .where("billId", "==", newData?.id) .get() console.log( @@ -81,8 +77,8 @@ export const populateNotificationEvents = functions.firestore .collection("/notificationEvents") .doc(notificationEventId) .update({ - history: newData?.history, - historyUpdateTime: Timestamp.now() + billHistory: newData?.history, + updateTime: Timestamp.now() }) } } diff --git a/functions/src/notifications/populateOrgNotificationEvents.ts b/functions/src/notifications/populateOrgNotificationEvents.ts new file mode 100644 index 000000000..ee3e5465b --- /dev/null +++ b/functions/src/notifications/populateOrgNotificationEvents.ts @@ -0,0 +1,90 @@ +// Sets up a document trigger for /events and queries the activeTopicSubscriptions collection group in Firestore +// for all subscriptions for the given topic event, then creates a notification document in the user's notification feed. +// This function runs every time a new topic event is created in the /events collection. +// Creates a notification document in the user's notification feed for each active subscription. + +// Import necessary Firebase modules +import * as functions from "firebase-functions" +import * as admin from "firebase-admin" +import { Timestamp } from "../firebase" +import { OrgNotification } from "./types" + +// Get a reference to the Firestore database +const db = admin.firestore() + +// Define the populateOrgNotificationEvents function +export const populateOrgNotificationEvents = functions.firestore + .document("/users/{userId}/publishedTestimony/{testimonyId}") + .onWrite(async (snapshot, context) => { + if (!snapshot.after.exists) { + console.error("New snapshot does not exist") + return + } + + const documentCreated = !snapshot.before.exists + + const oldData = snapshot.before.data() + const newData = snapshot.after.data() + + // New testimony added + if (documentCreated) { + console.log("New document created") + + const newNotificationEvent: OrgNotification = { + type: "org", + + billCourt: newData?.court.toString(), + billId: newData?.billId, + billName: newData?.billTitle, + + orgId: newData?.authorUid, + testimonyUser: newData?.fullName, + testimonyPosition: newData?.position, + testimonyContent: newData?.content, + testimonyVersion: newData?.version, + + updateTime: Timestamp.now() + } + + await db.collection("/notificationEvents").add(newNotificationEvent) + + return + } + + const oldVersion = oldData?.version + const newVersion = newData?.version + + const testimonyChanged = oldVersion !== newVersion + console.log(`oldVersion: ${oldVersion}, newVersion: ${newVersion}`) + + const notificationEventSnapshot = await db + .collection("/notificationEvents") + .where("type", "==", "org") + .where("billCourt", "==", newData?.court.toString()) + .where("billId", "==", newData?.billId) + .where("authorUid", "==", newData?.authorUid) + .get() + + console.log( + `${notificationEventSnapshot.docs} ${notificationEventSnapshot.docs.length}` + ) + + if (!notificationEventSnapshot.empty) { + const notificationEventId = notificationEventSnapshot.docs[0].id + + if (testimonyChanged) { + console.log("Testimony changed") + + // Update the existing notification event + await db + .collection("/notificationEvents") + .doc(notificationEventId) + .update({ + testimonyPosition: newData?.position, + testimonyContent: newData?.content, + testimonyVersion: newData?.version, + updateTime: Timestamp.now() + }) + } + } + }) diff --git a/functions/src/notifications/publishNotifications.ts b/functions/src/notifications/publishNotifications.ts index 43663255a..3afc12812 100644 --- a/functions/src/notifications/publishNotifications.ts +++ b/functions/src/notifications/publishNotifications.ts @@ -7,49 +7,57 @@ import * as functions from "firebase-functions" import * as admin from "firebase-admin" import { Timestamp } from "../firebase" +import { BillNotification, OrgNotification } from "./types" // Get a reference to the Firestore database const db = admin.firestore() const createNotificationFields = ( - entity: { - court: any - id: string - name: string - history: string - lastUpdatedTime: any - }, // history is an array, it needs to be concatenated - type: string + entity: BillNotification | OrgNotification ) => { - let topicName = "" - let header = "" - let court = null - switch (type) { + let topicName: string + let header: string + let court: string | null = null + let bodyText: string + let subheader: string + + switch (entity.type) { case "bill": - topicName = `bill-${entity.court}-${entity.id}` // looks for fields in event document - header = entity.name - court = entity.court + topicName = `bill-${entity.billCourt}-${entity.billId}` + header = entity.billId + court = entity.billCourt + if (entity.billHistory.length < 1) { + console.log(`Invalid history length: ${entity.billHistory.length}`) + throw new Error(`Invalid history length: ${entity.billHistory.length}`) + } + let lastHistoryAction = entity.billHistory[entity.billHistory.length - 1] + bodyText = `${lastHistoryAction.Action}` + subheader = `${lastHistoryAction.Branch}` break + case "org": - topicName = `org-${entity.id}` - header = entity.name + topicName = `org-${entity.orgId}` + header = entity.billName + court = entity.billCourt + bodyText = entity.testimonyContent + subheader = entity.testimonyUser break + default: - // handle exception for entities that don't fit schema - console.log(`Invalid entity type: ${type}`) - throw new Error(`Invalid entity type: ${type}`) + console.log(`Invalid entity: ${entity}`) + throw new Error(`Invalid entity: ${entity}`) } + return { - // set up notification document fields topicName, - uid: "", // user id will be populated in the publishNotifications function + uid: "", notification: { - bodyText: entity.history, // may change depending on event type + bodyText: bodyText, header, - id: entity.id, - subheader: "Do we need a sub heading", // may change depending on event type - timestamp: entity.lastUpdatedTime, // could also be fullDate ; might need to remove this all together - type, + id: entity.billId, + subheader: subheader, + timestamp: entity.updateTime, + type: entity.type, court, delivered: false }, @@ -62,7 +70,10 @@ export const publishNotifications = functions.firestore .document("/notificationEvents/{topicEventId}") .onWrite(async (snapshot, context) => { // Get the newly created topic event data - const topic = snapshot?.after.data() + const topic = snapshot?.after.data() as + | BillNotification + | OrgNotification + | undefined if (!topic) { console.error("Invalid topic data:", topic) @@ -70,58 +81,73 @@ export const publishNotifications = functions.firestore } // Extract related Bill or Org data from the topic event - const notificationPromises: any[] = [] + + // Create a batch + const batch = db.batch() console.log(`topic type: ${topic.type}`) - if (topic.type == "bill") { - console.log("bill") + const handleNotifications = async ( + topic: BillNotification | OrgNotification + ) => { + const notificationFields = createNotificationFields(topic) - const handleBillNotifications = async (topic: { - court: any - id: string - name: string - history: string - lastUpdatedTime: any - }) => { - const notificationFields = createNotificationFields(topic, "bill") + console.log(JSON.stringify(notificationFields)) - console.log(JSON.stringify(notificationFields)) + const topicNameSnapshot = await db + .collectionGroup("activeTopicSubscriptions") + .where("topicName", "==", notificationFields.topicName) + .get() - const subscriptionsSnapshot = await db + // Send a testimony notification to all users subscribed to the Bill + let billSnapshot + if (notificationFields.notification.type !== "bill") { + billSnapshot = await db .collectionGroup("activeTopicSubscriptions") - .where("topicName", "==", notificationFields.topicName) + .where( + "topicName", + "==", + `bill-${notificationFields.notification.court}-${notificationFields.notification.id}` + ) .get() + } - subscriptionsSnapshot.docs.forEach(doc => { - const subscription = doc.data() - const { uid } = subscription - - // Add the uid to the notification document - notificationFields.uid = uid + const uniqueDocs = new Map() - console.log( - `Pushing notifications to users/${uid}/userNotificationFeed` - ) + // Add documents from topicNameSnapshot to the Map + topicNameSnapshot.docs.forEach(doc => { + uniqueDocs.set(doc.data().uid, doc.data()) + }) - // Create a notification document in the user's notification feed - notificationPromises.push( - db - .collection(`users/${uid}/userNotificationFeed`) - .add(notificationFields) - ) + // If billSnapshot exists, add its documents to the Map + if (billSnapshot) { + billSnapshot.docs.forEach(doc => { + uniqueDocs.set(doc.data().uid, doc.data()) }) } - await handleBillNotifications({ - court: topic.court, - id: topic.id, - name: topic.name, - history: JSON.stringify(topic.history), - lastUpdatedTime: topic.historyUpdateTime + // Convert the Map values to an array to get the unique documents + const subscriptionsSnapshot = Array.from(uniqueDocs.values()) + + subscriptionsSnapshot.forEach(subscription => { + const { uid } = subscription + + // Add the uid to the notification document + notificationFields.uid = uid + + console.log( + `Pushing notifications to users/${uid}/userNotificationFeed` + ) + + // Get a reference to the new notification document + const docRef = db.collection(`users/${uid}/userNotificationFeed`).doc() + + // Add the write operation to the batch + batch.set(docRef, notificationFields) }) } - // Wait for all notification documents to be created - await Promise.all(notificationPromises) + await handleNotifications(topic) + + notificationPromises.push(batch.commit()) }) diff --git a/functions/src/notifications/types.ts b/functions/src/notifications/types.ts new file mode 100644 index 000000000..35144f003 --- /dev/null +++ b/functions/src/notifications/types.ts @@ -0,0 +1,24 @@ +import { BillHistory } from "../bills/types" +import { Timestamp } from "../firebase" + +export interface Notification { + type: string + updateTime: Timestamp + billCourt: string + billId: string + billName: string +} + +export type BillNotification = Notification & { + type: "bill" + billHistory: BillHistory +} + +export type OrgNotification = Notification & { + type: "org" + orgId: string + testimonyUser: string + testimonyPosition: string + testimonyContent: string + testimonyVersion: number +} diff --git a/pages/bills/index.tsx b/pages/bills/index.tsx index b82b64d99..45507492a 100644 --- a/pages/bills/index.tsx +++ b/pages/bills/index.tsx @@ -8,7 +8,7 @@ export default createPage({ Page: () => { return ( -

All Bills

+

Browse Bills

) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 16aa31a58..c138f9c7f 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -45,6 +45,16 @@ "newcomer" : "New to MAPLE? For extra support, check", "notInCommittee": "Bill not currently in committee", "orgs": "Organizations", + "partners": { + "header": "Our Partners", + "desc1": "The project is developed in partnership between the", + "desc2": "NuLawLab", + "desc3": "and scholars at", + "desc4": "Boston College Law School", + "desc5": "and", + "desc6": "Harvard University's Berkman Klein Center for Internet & Society", + "pid": "MAPLE is an initiative of Partners in Democracy - Education, a 501(c)(3) non profit organization. Partners In Democracy is building nationwide partnerships and exploring how we can drive democracy renovation efforts forward in states around the country — starting by focusing our efforts here at home in Massachusetts." + }, "pending_upgrade_warning": { "header": "Organization Request In Progress", "content": "Your request to be upgraded to an organization is currently in progress. You will be notified by email when your request has been reviewed." diff --git a/public/locales/en/footer.json b/public/locales/en/footer.json index 0070a42f3..58b211286 100644 --- a/public/locales/en/footer.json +++ b/public/locales/en/footer.json @@ -6,9 +6,9 @@ "resources": "Resources" }, "links": { - "learnWriting": "To Writing Effective Testimony", + "learnWriting": "To Write Effective Testimony", "learnProcess": "About the Legislative Process", - "learnWhy": "Why use MAPLE", + "learnWhy": "Why Use MAPLE", "supportMaple": "Support MAPLE", "ourMission": "Mission & Goals", "team": "Team", diff --git a/public/locales/en/supportmaple.json b/public/locales/en/supportmaple.json index c37e84764..c8fd6d5fa 100644 --- a/public/locales/en/supportmaple.json +++ b/public/locales/en/supportmaple.json @@ -2,9 +2,8 @@ "title": "How to Support MAPLE", "donate": { "header": "Donate", - "bodytextOne": "MAPLE is a fiscally sponsored initiative of the 501(c)(3), the Open Collective Foundation (OCF).", - "donorsLink": "You can see a full list of our donors and expenditures on our Open Collective webpage.", - "bodytextTwo": "This is where you can view the details of every donation we've received and every dollar we've spent - we are 100% transparent. We would be grateful for your financial support. " + "bodytextOne": "MAPLE is an initiative of Partners in Democracy - Education, a 501(c)(3) non profit organization. You can donate directly to the development of MAPLE through this ", + "donorsLink": "link" }, "volunteer": { "header": "Volunteer", diff --git a/public/pid.png b/public/pid.png new file mode 100644 index 000000000..7187bcab4 Binary files /dev/null and b/public/pid.png differ diff --git a/scripts/firebase-admin/backfillBillNotificationEvents.ts b/scripts/firebase-admin/backfillBillNotificationEvents.ts new file mode 100644 index 000000000..db44e498d --- /dev/null +++ b/scripts/firebase-admin/backfillBillNotificationEvents.ts @@ -0,0 +1,53 @@ +import { Timestamp } from "functions/src/firebase" +import { Script } from "./types" +import { BillNotification } from "functions/src/notifications/types" +import { Record, Number } from "runtypes" + +const Args = Record({ + court: Number +}) + +export const script: Script = async ({ db, args }) => { + console.log(args) + + const a = Args.check(args) + const court = a.court.toString() + + const snapshot = await db.collection(`/generalCourts/${court}/bills`).get() + + const batchLimit = 500 + let batch = db.batch() + let operationCount = 0 + + for (const doc of snapshot.docs) { + const data = doc.data() + + if (data) { + const notificationEvent: BillNotification = { + type: "bill", + + billCourt: court, + billId: data.id, + billName: data?.content.Title, + + billHistory: data.history, + + updateTime: Timestamp.now() + } + + const ref = db.collection("/notificationEvents").doc() + batch.set(ref, notificationEvent) + operationCount++ + + if (operationCount === batchLimit) { + await batch.commit() + batch = db.batch() + operationCount = 0 + } + } + } + + if (operationCount > 0) { + await batch.commit() + } +} diff --git a/scripts/firebase-admin/generateBillHistory.ts b/scripts/firebase-admin/generateBillHistory.ts new file mode 100644 index 000000000..6bc4ffaea --- /dev/null +++ b/scripts/firebase-admin/generateBillHistory.ts @@ -0,0 +1,40 @@ +import { Timestamp, FieldValue } from "../../functions/src/firebase" +import { Record, String, Number } from "runtypes" +import { Script } from "./types" +import { BillHistoryAction } from "../../functions/src/bills/types" + +const Args = Record({ + court: Number, + bills: String +}) + +export const script: Script = async ({ db, args }) => { + const a = Args.check(args) + const bills = a.bills.split(" ") + const court = a.court + let batch = db.batch() + let opsCounter = 0 + + for (const id of bills) { + const billHistoryAction: BillHistoryAction = { + Date: Timestamp.now().valueOf(), + Branch: Math.random() < 0.5 ? "Senate" : "House", + Action: (Math.random() + 1).toString(36).substring(2) + } + + const billRef = db.collection(`/generalCourts/${court}/bills`).doc(`${id}`) + batch.update(billRef, { history: FieldValue.arrayUnion(billHistoryAction) }) + opsCounter++ + + if (opsCounter % 500 === 0) { + await batch.commit() + batch = db.batch() + } + } + + if (opsCounter % 500 !== 0) { + await batch.commit() + } + + console.log(`Batch of ${opsCounter} bills updated.`) +}