Skip to content

Commit e2f43b8

Browse files
authored
Merge pull request #1912 from jicruz96/edit-profile-followers-tab
Show Users Their Own Follower Counts
1 parent 34641f9 commit e2f43b8

File tree

8 files changed

+188
-17
lines changed

8 files changed

+188
-17
lines changed

components/EditProfilePage/EditProfilePage.tsx

Lines changed: 16 additions & 4 deletions
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({
@@ -115,6 +116,7 @@ export function EditProfileForm({
115116
isOrg = isOrg || isPendingUpgrade
116117

117118
const { t } = useTranslation("editProfile")
119+
const [followerCount, setFollowerCount] = useState<number | null>(null)
118120

119121
const tabs = [
120122
{
@@ -147,6 +149,18 @@ export function EditProfileForm({
147149
title: t("tabs.following"),
148150
eventKey: "following",
149151
content: <FollowingTab className="mt-3 mb-4" />
152+
},
153+
{
154+
title: followerCount
155+
? t("tabs.followersWithCount", { count: followerCount })
156+
: t("tabs.followers"),
157+
eventKey: "followers",
158+
content: (
159+
<FollowersTab
160+
className="mt-3 mb-4"
161+
setFollowerCount={setFollowerCount}
162+
/>
163+
)
150164
}
151165
]
152166

@@ -174,9 +188,7 @@ export function EditProfileForm({
174188
>
175189
<TabNavWrapper>
176190
{tabs.map((t, i) => (
177-
<>
178-
<TabNavItem tab={t} i={i} />
179-
</>
191+
<TabNavItem key={i} tab={t} i={i} />
180192
))}
181193
</TabNavWrapper>
182194
<StyledTabContent>
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { functions } from "components/firebase"
2+
import { httpsCallable } from "firebase/functions"
3+
import { useTranslation } from "next-i18next"
4+
import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"
5+
import { useAuth } from "../auth"
6+
import { usePublicProfile } from "components/db"
7+
import { Internal } from "components/links"
8+
import { FollowUserButton } from "components/shared/FollowButton"
9+
import React from "react"
10+
import { Col, Row, Spinner, Stack, Alert } from "../bootstrap"
11+
import { TitledSectionCard } from "../shared"
12+
import { OrgIconSmall } from "./StyledEditProfileComponents"
13+
14+
export const FollowersTab = ({
15+
className,
16+
setFollowerCount
17+
}: {
18+
className?: string
19+
setFollowerCount: Dispatch<SetStateAction<number | null>>
20+
}) => {
21+
const uid = useAuth().user?.uid
22+
const [followerIds, setFollowerIds] = useState<string[]>([])
23+
const [loading, setLoading] = useState(true)
24+
const [error, setError] = useState<string | null>(null)
25+
const { t } = useTranslation("editProfile")
26+
27+
useEffect(() => {
28+
const fetchFollowers = async () => {
29+
try {
30+
const { data: followerIds } = await httpsCallable<void, string[]>(
31+
functions,
32+
"getFollowers"
33+
)()
34+
setFollowerIds(followerIds)
35+
setFollowerCount(followerIds.length)
36+
setLoading(false)
37+
} catch (err) {
38+
console.error("Error fetching followerIds", err)
39+
setError("Error fetching followers.")
40+
setLoading(false)
41+
return
42+
}
43+
}
44+
if (uid) fetchFollowers()
45+
}, [uid])
46+
return (
47+
<TitledSectionCard className={className}>
48+
<div className="mx-4 mt-3 d-flex flex-column gap-3">
49+
<Stack>
50+
<h2>{t("follow.your_followers")}</h2>
51+
<p className="mt-0 text-muted">
52+
{t("follow.follower_info_disclaimer")}
53+
</p>
54+
<div className="mt-3">
55+
{error ? (
56+
<Alert variant="danger">{error}</Alert>
57+
) : loading ? (
58+
<Spinner animation="border" className="mx-auto" />
59+
) : (
60+
followerIds.map((profileId, i) => (
61+
<FollowerCard key={i} profileId={profileId} />
62+
))
63+
)}
64+
</div>
65+
</Stack>
66+
</div>
67+
</TitledSectionCard>
68+
)
69+
}
70+
71+
const FollowerCard = ({ profileId }: { profileId: string }) => {
72+
const { result: profile, loading } = usePublicProfile(profileId)
73+
const { t } = useTranslation("profile")
74+
if (loading) {
75+
return (
76+
<FollowerCardWrapper>
77+
<Spinner animation="border" className="mx-auto" />
78+
</FollowerCardWrapper>
79+
)
80+
}
81+
const { fullName, profileImage, public: isPublic } = profile || {}
82+
const displayName = isPublic && fullName ? fullName : t("anonymousUser")
83+
return (
84+
<FollowerCardWrapper>
85+
<Col className="d-flex align-items-center flex-grow-1 p-0 text-start">
86+
<OrgIconSmall
87+
className="mr-4 mt-0 mb-0 ms-0"
88+
profileImage={profileImage}
89+
/>
90+
{isPublic ? (
91+
<Internal href={`/profile?id=${profileId}`}>{displayName}</Internal>
92+
) : (
93+
<span>{displayName}</span>
94+
)}
95+
</Col>
96+
{isPublic ? (
97+
<Col
98+
xs="auto"
99+
className="d-flex justify-content-end ms-auto text-end p-0"
100+
>
101+
<FollowUserButton profileId={profileId} />
102+
</Col>
103+
) : (
104+
<></>
105+
)}
106+
</FollowerCardWrapper>
107+
)
108+
}
109+
110+
const FollowerCardWrapper = ({ children }: { children: ReactNode }) => (
111+
<div className={`fs-3 lh-lg`}>
112+
<Row className="align-items-center justify-content-between g-0 w-100">
113+
{children}
114+
</Row>
115+
<hr className={`mt-3`} />
116+
</div>
117+
)

components/EditProfilePage/StyledEditProfileComponents.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const StyledTabNav = styled(Nav).attrs(props => ({
4242
export const TabNavWrapper = ({ children, className, ...props }: NavProps) => {
4343
return (
4444
<Nav
45-
className={`d-flex mb-3 text-center h3 color-dark ${className}`}
45+
className={`d-flex w-100 flex-column flex-lg-row flex-lg-nowrap mb-3 text-center h3 color-dark ${className}`}
4646
{...props}
4747
>
4848
{children}
@@ -68,12 +68,11 @@ export const TabNavItem = ({
6868
className?: string
6969
}) => {
7070
return (
71-
<Nav.Item
72-
className={`flex-grow-1 col-12 col-md-auto ${className}`}
73-
key={tab.eventKey}
74-
>
71+
<Nav.Item className={`flex-lg-fill ${className}`} key={tab.eventKey}>
7572
<TabNavLink eventKey={tab.eventKey} className={`rounded-top m-0 p-0`}>
76-
<p className={`my-0 ${i === 0 ? "" : "mx-4"}`}>{tab.title}</p>
73+
<p className={`my-0 text-nowrap ${i === 0 ? "" : "mx-4"}`}>
74+
{tab.title}
75+
</p>
7776
<hr className={`my-0`} />
7877
</TabNavLink>
7978
</Nav.Item>

functions/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ export {
4343
followBill,
4444
unfollowBill,
4545
followUser,
46-
unfollowUser
46+
unfollowUser,
47+
getFollowers
4748
} from "./subscriptions"
4849

4950
export { transcription } from "./webhooks"
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { getFirestore } from "firebase-admin/firestore"
2+
import * as functions from "firebase-functions"
3+
import { checkAuth } from "../common"
4+
5+
export const getFollowers = functions.https.onCall(async (_, context) => {
6+
const uid = checkAuth(context, false)
7+
8+
functions.logger.log(`[getFollowers] Finding followers for user UID: ${uid}`)
9+
10+
return await getFirestore()
11+
.collectionGroup("activeTopicSubscriptions")
12+
.where("userLookup.profileId", "==", uid)
13+
.get()
14+
.then(snapshot => {
15+
const followerIds = Array.from(
16+
new Set<string>(
17+
snapshot.docs
18+
.map(doc => doc.ref.parent.parent?.id)
19+
.filter((id): id is string => id !== uid)
20+
)
21+
)
22+
functions.logger.log(
23+
`[getFollowers] Found ${followerIds.length} followers for user UID: ${uid}`
24+
)
25+
return followerIds
26+
})
27+
.catch(error => {
28+
functions.logger.error("[getFollowers] Caught error:", error)
29+
throw new functions.https.HttpsError(
30+
"internal",
31+
"Failed to retrieve followers.",
32+
error
33+
)
34+
})
35+
})

functions/src/subscriptions/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ import { followBill } from "./followBill"
33
import { unfollowBill } from "./unfollowBill"
44
import { followUser } from "./followUser"
55
import { unfollowUser } from "./unfollowUser"
6-
6+
import { getFollowers } from "./getFollowers"
77
// export the functions
8-
export { followBill, unfollowBill, followUser, unfollowUser }
8+
export { followBill, unfollowBill, followUser, unfollowUser, getFollowers }

pages/edit-profile/[[...docName]].tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export const getStaticPaths: GetStaticPaths = async ctx => {
2626
{ params: { docName: ["about-you"] } },
2727
{ params: { docName: ["testimonies"] } },
2828
{ params: { docName: ["following"] } },
29+
{ params: { docName: ["followers"] } },
2930
{ params: { docName: [] } }
3031
],
3132
fallback: false
@@ -36,6 +37,7 @@ export const getStaticProps = createGetStaticTranslationProps([
3637
"auth",
3738
"common",
3839
"editProfile",
40+
"profile",
3941
"footer",
4042
"testimony"
4143
])

public/locales/en/editProfile.json

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@
1515
"tabs": {
1616
"personalInfo": "Personal Information",
1717
"testimonies": "Testimonies",
18-
"following": "Following"
18+
"following": "Following",
19+
"followers": "Followers",
20+
"followersWithCount": "Followers ({{count}})"
1921
},
2022
"content": {
2123
"reqPending": "Your request to be an organization account is pending approval",
@@ -25,7 +27,10 @@
2527
"follow": {
2628
"bills": "Bills I Follow",
2729
"orgs": "Users I Follow",
28-
"unfollow": "Unfollow"
30+
"your_followers": "Your Followers",
31+
"unfollow": "Unfollow",
32+
"follow": "Follow",
33+
"follower_info_disclaimer": "Names and follower counts are not publicly available; only visible to you."
2934
},
3035
"confirmation": {
3136
"unfollowMessage": "Are you sure you want to unfollow",
@@ -38,7 +43,7 @@
3843
"weekly": "Weekly",
3944
"monthly": "Monthly"
4045
},
41-
"emailIconAlt":"open envelope with letter, toggles update frequency options",
46+
"emailIconAlt": "open envelope with letter, toggles update frequency options",
4247
"legislator": {
4348
"representative": "Representative",
4449
"searchRepresentative": "Search your representative",
@@ -100,4 +105,4 @@
100105
"admin": "Your profile is currently public. Others can view your profile page. Admin accounts cannot set their profile to private.",
101106
"default": "The publicity of your profile is unknown. Please contact the admin for more information."
102107
}
103-
}
108+
}

0 commit comments

Comments
 (0)