Skip to content

Commit 6cf7fb6

Browse files
authored
Merge pull request #2098 from codeforboston/main
Deploy to Prod 4/6/26
2 parents 1cbbfa4 + b782893 commit 6cf7fb6

29 files changed

Lines changed: 908 additions & 746 deletions

components/EditProfilePage/EditProfileHeader.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,25 @@ import { Role } from "../auth"
33
import { Col, Row } from "../bootstrap"
44
import { GearIcon, OutlineButton } from "../buttons"
55
import { ProfileEditToggle } from "components/ProfilePage/ProfileButtons"
6+
import { useFlags } from "components/featureFlags"
67

78
export const EditProfileHeader = ({
89
formUpdated,
910
onSettingsModalOpen,
11+
onGetVerifiedClick,
1012
uid,
11-
role
13+
role,
14+
phoneVerified
1215
}: {
1316
formUpdated: boolean
1417
onSettingsModalOpen: () => void
18+
onGetVerifiedClick?: () => void
1519
uid: string
1620
role: Role
21+
phoneVerified?: boolean
1722
}) => {
1823
const { t } = useTranslation("editProfile")
24+
const { phoneVerificationUI } = useFlags()
1925

2026
return (
2127
<Row className={`my-5`}>
@@ -30,6 +36,25 @@ export const EditProfileHeader = ({
3036
onClick={() => onSettingsModalOpen()}
3137
/>
3238
<ProfileEditToggle formUpdated={formUpdated} role={role} uid={uid} />
39+
{phoneVerificationUI &&
40+
(phoneVerified === true ? (
41+
<div className="d-flex align-items-center justify-content-center gap-1 py-1 col-12 text-capitalize text-nowrap">
42+
<span className="text-secondary">{t("verifiedUser")}</span>
43+
<img
44+
src="/images/verifiedUser.png"
45+
alt={t("verifiedUserBadgeAlt")}
46+
width={24}
47+
height={24}
48+
className="flex-shrink-0"
49+
/>
50+
</div>
51+
) : onGetVerifiedClick ? (
52+
<OutlineButton
53+
className={`py-1`}
54+
label={t("getVerified")}
55+
onClick={onGetVerifiedClick}
56+
/>
57+
) : null)}
3358
</Col>
3459
</Row>
3560
)

components/EditProfilePage/EditProfilePage.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { EditProfileHeader } from "./EditProfileHeader"
1818
import { FollowingTab } from "./FollowingTab"
1919
import { PersonalInfoTab } from "./PersonalInfoTab"
20+
import PhoneVerificationModal from "./PhoneVerificationModal"
2021
import ProfileSettingsModal from "./ProfileSettingsModal"
2122
import {
2223
StyledTabContent,
@@ -87,6 +88,8 @@ export function EditProfileForm({
8788

8889
const [formUpdated, setFormUpdated] = useState(false)
8990
const [settingsModal, setSettingsModal] = useState<"show" | null>(null)
91+
const [showPhoneVerificationModal, setShowPhoneVerificationModal] =
92+
useState(false)
9093
const [notifications, setNotifications] = useState<Frequency>(
9194
notificationFrequency || "Weekly"
9295
)
@@ -178,8 +181,10 @@ export function EditProfileForm({
178181
<EditProfileHeader
179182
formUpdated={formUpdated}
180183
onSettingsModalOpen={onSettingsModalOpen}
184+
onGetVerifiedClick={() => setShowPhoneVerificationModal(true)}
181185
uid={uid}
182186
role={profile.role}
187+
phoneVerified={profile.phoneVerified}
183188
/>
184189
<TabContainer
185190
defaultActiveKey="about-you"
@@ -211,6 +216,10 @@ export function EditProfileForm({
211216
onSettingsModalClose={() => setSettingsModal(null)}
212217
show={settingsModal === "show"}
213218
/>
219+
<PhoneVerificationModal
220+
show={showPhoneVerificationModal}
221+
onHide={() => setShowPhoneVerificationModal(false)}
222+
/>
214223
</>
215224
)
216225
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { usePublicProfile } from "components/db"
2+
import { Internal } from "components/links"
3+
import { FollowUserButton } from "components/shared/FollowButton"
4+
import { useTranslation } from "next-i18next"
5+
import React from "react"
6+
import { Col, Row, Spinner } from "../bootstrap"
7+
import { OrgIconSmall } from "./StyledEditProfileComponents"
8+
9+
export function FollowUserCard({
10+
profileId,
11+
confirmUnfollow
12+
}: {
13+
profileId: string
14+
confirmUnfollow?: boolean
15+
}) {
16+
const { result: profile, loading } = usePublicProfile(profileId)
17+
const { t } = useTranslation("profile")
18+
19+
if (loading) {
20+
return (
21+
<div className={`fs-3 lh-lg`}>
22+
<Row className="align-items-center justify-content-between g-0 w-100">
23+
<Spinner animation="border" className="mx-auto" />
24+
</Row>
25+
<hr className={`mt-3`} />
26+
</div>
27+
)
28+
}
29+
30+
const { fullName, profileImage, public: isPublic } = profile || {}
31+
const displayName = isPublic && fullName ? fullName : t("anonymousUser")
32+
33+
return (
34+
<div className={`fs-3 lh-lg`}>
35+
<Row className="align-items-center justify-content-between g-0 w-100">
36+
<Col className="d-flex align-items-center flex-grow-1 p-0 text-start">
37+
<OrgIconSmall
38+
className="mr-4 mt-0 mb-0 ms-0"
39+
profileImage={profileImage}
40+
/>
41+
{isPublic ? (
42+
<Internal href={`/profile?id=${profileId}`}>{displayName}</Internal>
43+
) : (
44+
<span>{displayName}</span>
45+
)}
46+
</Col>
47+
{isPublic ? (
48+
<Col xs="auto" className="d-flex justify-content-end ms-auto p-0">
49+
<FollowUserButton
50+
profileId={profileId}
51+
confirmUnfollow={confirmUnfollow}
52+
/>
53+
</Col>
54+
) : null}
55+
</Row>
56+
<hr className={`mt-3`} />
57+
</div>
58+
)
59+
}
Lines changed: 46 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { functions } from "components/firebase"
22
import { httpsCallable } from "firebase/functions"
33
import { useTranslation } from "next-i18next"
4-
import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"
4+
import { Dispatch, SetStateAction, useEffect, useState } from "react"
55
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"
6+
import { FollowUserCard } from "./FollowUserCard"
7+
import {
8+
LoadableItemsState,
9+
PaginatedItemsCard
10+
} from "components/shared/PaginatedItemsCard"
1311

1412
export const FollowersTab = ({
1513
className,
@@ -19,99 +17,51 @@ export const FollowersTab = ({
1917
setFollowerCount: Dispatch<SetStateAction<number | null>>
2018
}) => {
2119
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)
20+
const [state, setState] = useState<LoadableItemsState<{ profileId: string }>>(
21+
{
22+
items: [],
23+
loading: true,
24+
error: null
25+
}
26+
)
2527
const { t } = useTranslation("editProfile")
2628

29+
const fetchFollowers = async () => {
30+
try {
31+
const { data: profileIds } = await httpsCallable<void, string[]>(
32+
functions,
33+
"getFollowers"
34+
)()
35+
setState({
36+
items: profileIds.map(profileId => ({ profileId })),
37+
loading: false,
38+
error: null
39+
})
40+
setFollowerCount(profileIds.length)
41+
} catch (err) {
42+
console.error("Error fetching followerIds", err)
43+
setState({
44+
items: [],
45+
loading: false,
46+
error: t("content.error")
47+
})
48+
}
49+
}
2750
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-
}
51+
if (uid) {
52+
setState(prev => ({ ...prev, loading: true, error: null }))
53+
fetchFollowers()
54+
} else {
55+
setState({ items: [], loading: false, error: null })
4356
}
44-
if (uid) fetchFollowers()
4557
}, [uid])
4658
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>
59+
<PaginatedItemsCard
60+
className={className}
61+
title={t("follow.your_followers")}
62+
description={t("follow.follower_info_disclaimer")}
63+
ItemCard={FollowUserCard}
64+
{...state}
65+
/>
10766
)
10867
}
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-
)

0 commit comments

Comments
 (0)