Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion components/EditProfilePage/EditProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@ import { Role } from "../auth"
import { Col, Row } from "../bootstrap"
import { GearIcon, OutlineButton } from "../buttons"
import { ProfileEditToggle } from "components/ProfilePage/ProfileButtons"
import { useFlags } from "components/featureFlags"

export const EditProfileHeader = ({
formUpdated,
onSettingsModalOpen,
onGetVerifiedClick,
uid,
role
role,
phoneVerified
}: {
formUpdated: boolean
onSettingsModalOpen: () => void
onGetVerifiedClick?: () => void
uid: string
role: Role
phoneVerified?: boolean
}) => {
const { t } = useTranslation("editProfile")
const { phoneVerificationUI } = useFlags()

return (
<Row className={`my-5`}>
Expand All @@ -30,6 +36,25 @@ export const EditProfileHeader = ({
onClick={() => onSettingsModalOpen()}
/>
<ProfileEditToggle formUpdated={formUpdated} role={role} uid={uid} />
{phoneVerificationUI &&
(phoneVerified === true ? (
<div className="d-flex align-items-center justify-content-center gap-1 py-1 col-12 text-capitalize text-nowrap">
<span className="text-secondary">{t("verifiedUser")}</span>
<img
src="/images/verifiedUser.png"
alt={t("verifiedUserBadgeAlt")}
width={24}
height={24}
className="flex-shrink-0"
/>
</div>
) : onGetVerifiedClick ? (
<OutlineButton
className={`py-1`}
label={t("getVerified")}
onClick={onGetVerifiedClick}
/>
) : null)}
</Col>
</Row>
)
Expand Down
9 changes: 9 additions & 0 deletions components/EditProfilePage/EditProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { EditProfileHeader } from "./EditProfileHeader"
import { FollowingTab } from "./FollowingTab"
import { PersonalInfoTab } from "./PersonalInfoTab"
import PhoneVerificationModal from "./PhoneVerificationModal"
import ProfileSettingsModal from "./ProfileSettingsModal"
import {
StyledTabContent,
Expand Down Expand Up @@ -87,6 +88,8 @@ export function EditProfileForm({

const [formUpdated, setFormUpdated] = useState(false)
const [settingsModal, setSettingsModal] = useState<"show" | null>(null)
const [showPhoneVerificationModal, setShowPhoneVerificationModal] =
useState(false)
const [notifications, setNotifications] = useState<Frequency>(
notificationFrequency || "Weekly"
)
Expand Down Expand Up @@ -178,8 +181,10 @@ export function EditProfileForm({
<EditProfileHeader
formUpdated={formUpdated}
onSettingsModalOpen={onSettingsModalOpen}
onGetVerifiedClick={() => setShowPhoneVerificationModal(true)}
uid={uid}
role={profile.role}
phoneVerified={profile.phoneVerified}
/>
<TabContainer
defaultActiveKey="about-you"
Expand Down Expand Up @@ -211,6 +216,10 @@ export function EditProfileForm({
onSettingsModalClose={() => setSettingsModal(null)}
show={settingsModal === "show"}
/>
<PhoneVerificationModal
show={showPhoneVerificationModal}
onHide={() => setShowPhoneVerificationModal(false)}
/>
</>
)
}
59 changes: 59 additions & 0 deletions components/EditProfilePage/FollowUserCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { usePublicProfile } from "components/db"
import { Internal } from "components/links"
import { FollowUserButton } from "components/shared/FollowButton"
import { useTranslation } from "next-i18next"
import React from "react"
import { Col, Row, Spinner } from "../bootstrap"
import { OrgIconSmall } from "./StyledEditProfileComponents"

export function FollowUserCard({
profileId,
confirmUnfollow
}: {
profileId: string
confirmUnfollow?: boolean
}) {
const { result: profile, loading } = usePublicProfile(profileId)
const { t } = useTranslation("profile")

if (loading) {
return (
<div className={`fs-3 lh-lg`}>
<Row className="align-items-center justify-content-between g-0 w-100">
<Spinner animation="border" className="mx-auto" />
</Row>
<hr className={`mt-3`} />
</div>
)
}

const { fullName, profileImage, public: isPublic } = profile || {}
const displayName = isPublic && fullName ? fullName : t("anonymousUser")

return (
<div className={`fs-3 lh-lg`}>
<Row className="align-items-center justify-content-between g-0 w-100">
<Col className="d-flex align-items-center flex-grow-1 p-0 text-start">
<OrgIconSmall
className="mr-4 mt-0 mb-0 ms-0"
profileImage={profileImage}
/>
{isPublic ? (
<Internal href={`/profile?id=${profileId}`}>{displayName}</Internal>
) : (
<span>{displayName}</span>
)}
</Col>
{isPublic ? (
<Col xs="auto" className="d-flex justify-content-end ms-auto p-0">
<FollowUserButton
profileId={profileId}
confirmUnfollow={confirmUnfollow}
/>
</Col>
) : null}
</Row>
<hr className={`mt-3`} />
</div>
)
}
142 changes: 46 additions & 96 deletions components/EditProfilePage/FollowersTab.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { functions } from "components/firebase"
import { httpsCallable } from "firebase/functions"
import { useTranslation } from "next-i18next"
import { Dispatch, ReactNode, SetStateAction, useEffect, useState } from "react"
import { Dispatch, SetStateAction, useEffect, useState } from "react"
import { useAuth } from "../auth"
import { usePublicProfile } from "components/db"
import { Internal } from "components/links"
import { FollowUserButton } from "components/shared/FollowButton"
import React from "react"
import { Col, Row, Spinner, Stack, Alert } from "../bootstrap"
import { TitledSectionCard } from "../shared"
import { OrgIconSmall } from "./StyledEditProfileComponents"
import { FollowUserCard } from "./FollowUserCard"
import {
LoadableItemsState,
PaginatedItemsCard
} from "components/shared/PaginatedItemsCard"

export const FollowersTab = ({
className,
Expand All @@ -19,99 +17,51 @@ export const FollowersTab = ({
setFollowerCount: Dispatch<SetStateAction<number | null>>
}) => {
const uid = useAuth().user?.uid
const [followerIds, setFollowerIds] = useState<string[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [state, setState] = useState<LoadableItemsState<{ profileId: string }>>(
{
items: [],
loading: true,
error: null
}
)
const { t } = useTranslation("editProfile")

const fetchFollowers = async () => {
try {
const { data: profileIds } = await httpsCallable<void, string[]>(
functions,
"getFollowers"
)()
setState({
items: profileIds.map(profileId => ({ profileId })),
loading: false,
error: null
})
setFollowerCount(profileIds.length)
} catch (err) {
console.error("Error fetching followerIds", err)
setState({
items: [],
loading: false,
error: t("content.error")
})
}
}
useEffect(() => {
const fetchFollowers = async () => {
try {
const { data: followerIds } = await httpsCallable<void, string[]>(
functions,
"getFollowers"
)()
setFollowerIds(followerIds)
setFollowerCount(followerIds.length)
setLoading(false)
} catch (err) {
console.error("Error fetching followerIds", err)
setError("Error fetching followers.")
setLoading(false)
return
}
if (uid) {
setState(prev => ({ ...prev, loading: true, error: null }))
fetchFollowers()
} else {
setState({ items: [], loading: false, error: null })
}
if (uid) fetchFollowers()
}, [uid])
return (
<TitledSectionCard className={className}>
<div className="mx-4 mt-3 d-flex flex-column gap-3">
<Stack>
<h2>{t("follow.your_followers")}</h2>
<p className="mt-0 text-muted">
{t("follow.follower_info_disclaimer")}
</p>
<div className="mt-3">
{error ? (
<Alert variant="danger">{error}</Alert>
) : loading ? (
<Spinner animation="border" className="mx-auto" />
) : (
followerIds.map((profileId, i) => (
<FollowerCard key={i} profileId={profileId} />
))
)}
</div>
</Stack>
</div>
</TitledSectionCard>
)
}

const FollowerCard = ({ profileId }: { profileId: string }) => {
const { result: profile, loading } = usePublicProfile(profileId)
const { t } = useTranslation("profile")
if (loading) {
return (
<FollowerCardWrapper>
<Spinner animation="border" className="mx-auto" />
</FollowerCardWrapper>
)
}
const { fullName, profileImage, public: isPublic } = profile || {}
const displayName = isPublic && fullName ? fullName : t("anonymousUser")
return (
<FollowerCardWrapper>
<Col className="d-flex align-items-center flex-grow-1 p-0 text-start">
<OrgIconSmall
className="mr-4 mt-0 mb-0 ms-0"
profileImage={profileImage}
/>
{isPublic ? (
<Internal href={`/profile?id=${profileId}`}>{displayName}</Internal>
) : (
<span>{displayName}</span>
)}
</Col>
{isPublic ? (
<Col
xs="auto"
className="d-flex justify-content-end ms-auto text-end p-0"
>
<FollowUserButton profileId={profileId} />
</Col>
) : (
<></>
)}
</FollowerCardWrapper>
<PaginatedItemsCard
className={className}
title={t("follow.your_followers")}
description={t("follow.follower_info_disclaimer")}
ItemCard={FollowUserCard}
{...state}
/>
)
}

const FollowerCardWrapper = ({ children }: { children: ReactNode }) => (
<div className={`fs-3 lh-lg`}>
<Row className="align-items-center justify-content-between g-0 w-100">
{children}
</Row>
<hr className={`mt-3`} />
</div>
)
Loading
Loading