Skip to content

Commit e5b0686

Browse files
committed
Merge branch 'main' into search-component-translations
2 parents 1224ff6 + c8e7c15 commit e5b0686

File tree

14 files changed

+180
-244
lines changed

14 files changed

+180
-244
lines changed

components/CurrentCommitteeCard/CurrentCommitteeCard.tsx

Lines changed: 0 additions & 46 deletions
This file was deleted.

components/EditProfilePage/EditProfilePage.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@ import { TestimoniesTab } from "./TestimoniesTab"
2727
import { useFlags } from "components/featureFlags"
2828
import LoginPage from "components/Login/login"
2929
import { PendingUpgradeBanner } from "components/PendingUpgradeBanner"
30+
import { FollowersTab } from "./FollowersTab"
3031

31-
const tabTitle = ["about-you", "testimonies", "following"] as const
32+
const tabTitle = ["about-you", "testimonies", "following", "followers"] as const
3233
export type TabTitles = (typeof tabTitle)[number]
3334

3435
export default function EditProfile({
@@ -145,6 +146,11 @@ export function EditProfileForm({
145146
title: t("tabs.following"),
146147
eventKey: "following",
147148
content: <FollowingTab className="mt-3 mb-4" />
149+
},
150+
{
151+
title: t("followersWithCount", { count: 1 }),
152+
eventKey: "followers",
153+
content: <FollowersTab className="mt-3 mb-4" />
148154
}
149155
]
150156

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { useTranslation } from "next-i18next"
2+
import { useEffect, useMemo, useState } from "react"
3+
import { useAuth } from "../auth"
4+
import { Stack } from "../bootstrap"
5+
import { firestore } from "../firebase"
6+
import {
7+
collection,
8+
collectionGroup,
9+
getDocs,
10+
query,
11+
where,
12+
addDoc
13+
} from "firebase/firestore"
14+
import { FollowedItem, UserElement } from "./FollowingTabComponents"
15+
import { UnfollowModalConfig } from "./UnfollowModal"
16+
17+
export const FollowersTab = ({ className }: { className?: string }) => {
18+
const { user } = useAuth()
19+
const uid = user?.uid
20+
21+
// NEW: build a query over the collection group
22+
const followersQuery = useMemo(
23+
() =>
24+
uid
25+
? query(
26+
collectionGroup(firestore, "activeTopicSubscriptions"),
27+
where("uid", "==", uid), // they follow *me*
28+
where("type", "==", "testimony") // user-to-user follows only
29+
)
30+
: null,
31+
[uid]
32+
)
33+
34+
// query for people *I* follow so we can decide button state
35+
const myFollowsQuery = useMemo(
36+
() =>
37+
uid
38+
? query(
39+
collection(firestore, `/users/${uid}/activeTopicSubscriptions/`),
40+
where("uid", "==", uid),
41+
where("type", "==", "testimony")
42+
)
43+
: null,
44+
[uid]
45+
)
46+
const [iFollowSet, setIFollowSet] = useState<Set<string>>(new Set())
47+
48+
const [followers, setFollowers] = useState<UserElement[]>([])
49+
50+
useEffect(() => {
51+
if (!followersQuery) return
52+
;(async () => {
53+
const qs = await getDocs(followersQuery)
54+
const list: UserElement[] = []
55+
qs.forEach(doc => list.push(doc.data().userLookup)) // same field used elsewhere
56+
setFollowers(list)
57+
})()
58+
}, [followersQuery])
59+
60+
useEffect(() => {
61+
if (!myFollowsQuery) return
62+
;(async () => {
63+
const qs = await getDocs(myFollowsQuery)
64+
const ids = new Set<string>()
65+
qs.forEach(doc => ids.add(doc.data().userLookup.uid))
66+
setIFollowSet(ids)
67+
})()
68+
}, [myFollowsQuery])
69+
70+
const [unfollow, setUnfollow] = useState<UnfollowModalConfig | null>(null)
71+
72+
const { t } = useTranslation("editProfile")
73+
74+
return (
75+
<Stack>
76+
<h2 className={className ? `pb-3 ${className}` : "pb-3"}>
77+
{t("follow.orgs")}
78+
</h2>
79+
{followers.map(element => (
80+
<FollowedItem
81+
key={element.profileId}
82+
element={element}
83+
alreadyFollowing={iFollowSet.has(element.profileId)}
84+
setFollow={async (targetUid: string) => {
85+
if (!uid || iFollowSet.has(targetUid)) return
86+
await addDoc(
87+
collection(firestore, `/users/${uid}/activeTopicSubscriptions/`),
88+
{
89+
uid,
90+
type: "testimony",
91+
userLookup: { uid: targetUid }
92+
}
93+
)
94+
setIFollowSet(new Set([...iFollowSet, targetUid]))
95+
}}
96+
setUnfollow={setUnfollow}
97+
type="org"
98+
/>
99+
))}
100+
</Stack>
101+
)
102+
}

components/formatting.tsx

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import { Timestamp } from "firebase/firestore"
2-
import { useMediaQuery } from "usehooks-ts"
3-
import { Testimony } from "../functions/src/testimony/types"
4-
import { Bill, BillContent } from "./db"
5-
61
const billIdFormat = /^(?<chamber>\D+)(?<number>\d+)$/
72

83
/** Formats H123 as H.123 */
@@ -15,56 +10,5 @@ export const formatBillId = (id: string) => {
1510
}
1611
}
1712

18-
const MISSING_TIMESTAMP = Timestamp.fromMillis(0)
19-
export const formatTimestamp = (t?: Timestamp) => {
20-
if (!t || t.toMillis() == MISSING_TIMESTAMP.toMillis()) {
21-
return undefined
22-
}
23-
return t.toDate().toLocaleDateString()
24-
}
25-
26-
export const FormattedBillTitle = ({ bill }: { bill: Bill | BillContent }) => {
27-
const isMobile = useMediaQuery("(max-width: 768px)")
28-
const billInfo = "content" in bill ? bill.content : bill
29-
30-
const { BillNumber, Title } = billInfo
31-
32-
return (
33-
<div className="mt-2">
34-
{formatBillId(BillNumber)}:{" "}
35-
{isMobile ? Title.substring(0, 45) + "..." : Title}
36-
</div>
37-
)
38-
}
39-
40-
export const FormattedTestimonyTitle = ({
41-
testimony
42-
}: {
43-
testimony: Testimony
44-
}) => {
45-
const { authorDisplayName, publishedAt, position } = testimony
46-
47-
return (
48-
<div>
49-
<span>
50-
<b>Author:</b> {authorDisplayName || "anonymous"}
51-
</span>
52-
<br></br>
53-
<span>
54-
<b>Published on:</b> {publishedAt.toDate().toLocaleDateString()}
55-
</span>
56-
<br></br>
57-
<span>
58-
<b>Position:</b> {position}
59-
</span>
60-
</div>
61-
)
62-
}
63-
64-
export const decodeHtmlCharCodes = (s: string) =>
65-
s.replace(/(&#(\d+);)/g, (match, capture, charCode) =>
66-
String.fromCharCode(charCode)
67-
)
68-
6913
export const truncateText = (s: string | undefined, maxLength: number) =>
7014
!!s && s.length > maxLength ? s.substring(0, maxLength) + "..." : s

components/search/bills/BillSearch.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,21 @@ import {
77
SearchBox,
88
useInstantSearch
99
} from "react-instantsearch"
10-
import { createInstantSearchRouterNext } from "react-instantsearch-router-nextjs"
11-
import singletonRouter from "next/router"
10+
import { currentGeneralCourt } from "functions/src/shared"
1211
import styled from "styled-components"
1312
import TypesenseInstantSearchAdapter from "typesense-instantsearch-adapter"
14-
import { currentGeneralCourt } from "functions/src/shared"
1513
import { Col, Container, Row, Spinner } from "../../bootstrap"
1614
import { NoResults } from "../NoResults"
1715
import { ResultCount } from "../ResultCount"
1816
import { SearchContainer } from "../SearchContainer"
1917
import { SearchErrorBoundary } from "../SearchErrorBoundary"
18+
import { useRouting } from "../useRouting"
2019
import { BillHit } from "./BillHit"
2120
import { useBillRefinements } from "./useBillRefinements"
2221
import { SortBy, SortByWithConfigurationItem } from "../SortBy"
2322
import { getServerConfig, VirtualFilters } from "../common"
2423
import { useBillSort } from "./useBillSort"
2524
import { FC, useState } from "react"
26-
import { pathToSearchState, searchStateToUrl } from "../routingHelpers"
2725

2826
const searchClient = new TypesenseInstantSearchAdapter({
2927
server: getServerConfig(),
@@ -72,16 +70,8 @@ export const BillSearch = () => {
7270
}
7371
}}
7472
searchClient={searchClient}
75-
routing={{
76-
router: createInstantSearchRouterNext({
77-
singletonRouter,
78-
routerOptions: {
79-
cleanUrlOnDispose: false,
80-
createURL: args => searchStateToUrl(args),
81-
parseURL: args => pathToSearchState(args)
82-
}
83-
})
84-
}}
73+
routing={useRouting()}
74+
future={{ preserveSharedStateOnUnmount: true }}
8575
>
8676
<VirtualFilters type="bill" />
8777
<Layout items={items} />

components/search/routingHelpers.ts

Lines changed: 0 additions & 36 deletions
This file was deleted.

components/search/testimony/TestimonySearch.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import {
66
SearchBox,
77
useInstantSearch
88
} from "react-instantsearch"
9-
import { createInstantSearchRouterNext } from "react-instantsearch-router-nextjs"
10-
import singletonRouter from "next/router"
119
import {
1210
StyledTabContent,
1311
StyledTabNav
@@ -25,10 +23,10 @@ import { SearchContainer } from "../SearchContainer"
2523
import { SearchErrorBoundary } from "../SearchErrorBoundary"
2624
import { SortBy } from "../SortBy"
2725
import { getServerConfig, VirtualFilters } from "../common"
26+
import { useRouting } from "../useRouting"
2827
import { TestimonyHit } from "./TestimonyHit"
2928
import { useTestimonyRefinements } from "./useTestimonyRefinements"
3029
import { FollowContext, OrgFollowStatus } from "components/shared/FollowContext"
31-
import { pathToSearchState, searchStateToUrl } from "../routingHelpers"
3230
import { useTranslation } from "next-i18next"
3331

3432
const searchClient = new TypesenseInstantSearchAdapter({
@@ -73,16 +71,8 @@ export const TestimonySearch = () => {
7371
}
7472
}}
7573
searchClient={searchClient}
76-
routing={{
77-
router: createInstantSearchRouterNext({
78-
singletonRouter,
79-
routerOptions: {
80-
cleanUrlOnDispose: false,
81-
createURL: args => searchStateToUrl(args),
82-
parseURL: args => pathToSearchState(args)
83-
}
84-
})
85-
}}
74+
routing={useRouting()}
75+
future={{ preserveSharedStateOnUnmount: true }}
8676
>
8777
<VirtualFilters type="testimony" />
8878
<Layout />

0 commit comments

Comments
 (0)