diff --git a/src/app/(marketing)/profile/[username]/page.tsx b/src/app/(marketing)/profile/[username]/page.tsx index 4b0518641c..2e0dd3039c 100644 --- a/src/app/(marketing)/profile/[username]/page.tsx +++ b/src/app/(marketing)/profile/[username]/page.tsx @@ -7,7 +7,7 @@ import { LayoutMain, } from "@/components/Layout" import { SWRProvider } from "@/components/SWRProvider" -import { Guild, Role, Schemas } from "@guildxyz/types" +import { FarcasterProfile, Guild, Role, Schemas } from "@guildxyz/types" import { env } from "env" import { Metadata } from "next" import Image from "next/image" @@ -60,6 +60,33 @@ const fetchPublicProfileData = async ({ if (!fetchFallback) { return { profile } } + const farcasterProfilesRequest = new URL( + `/v2/users/${profile.userId}/farcaster-profiles`, + api + ) + const farcasterProfiles = await ssrFetcher( + farcasterProfilesRequest, + { + next: { + tags: ["profile"], + revalidate: 3600, + }, + } + ) + const fcProfile = farcasterProfiles.at(0) + const neynarRequest = + fcProfile && + new URL( + `https://api.neynar.com/v2/farcaster/user/bulk?api_key=NEYNAR_API_DOCS&fids=${fcProfile.fid}` + ) + const fcFollowers = + neynarRequest && + (await ssrFetcher(neynarRequest, { + next: { + revalidate: 24 * 3600, + }, + })) + const referredUsersRequest = new URL( `/v2/profiles/${username}/referred-users`, api @@ -104,26 +131,26 @@ const fetchPublicProfileData = async ({ }) ) ) - const guildsZipped = Object.fromEntries( - guildRequests.map(({ pathname }, i) => [pathname, guilds[i]]) - ) - const rolesZipped = Object.fromEntries( - roleRequests.map(({ pathname }, i) => [pathname, roles[i]]) - ) + const guildsZipped = guildRequests.map(({ pathname }, i) => [pathname, guilds[i]]) + const rolesZipped = roleRequests.map(({ pathname }, i) => [pathname, roles[i]]) return { profile, - fallback: { - [profileRequest.pathname]: profile, - [contributionsRequest.pathname]: contributions, - [referredUsersRequest.pathname]: referredUsers, - ...guildsZipped, - ...rolesZipped, - }, + fallback: Object.fromEntries( + [ + [profileRequest.pathname, profile], + [contributionsRequest.pathname, contributions], + [farcasterProfilesRequest.pathname, farcasterProfiles], + [neynarRequest?.href, fcFollowers], + [referredUsersRequest.pathname, referredUsers], + ...guildsZipped, + ...rolesZipped, + ].filter(([key, value]) => key && value) + ), } } const Page = async ({ params: { username } }: PageProps) => { - const { fallback, profile } = await fetchPublicProfileData({ username }) + const { profile, fallback } = await fetchPublicProfileData({ username }) const isBgColor = profile.backgroundImageUrl?.startsWith("#") diff --git a/src/app/(marketing)/profile/_components/ProfileHero.tsx b/src/app/(marketing)/profile/_components/ProfileHero.tsx index 4f514cd674..eb993e2088 100644 --- a/src/app/(marketing)/profile/_components/ProfileHero.tsx +++ b/src/app/(marketing)/profile/_components/ProfileHero.tsx @@ -1,5 +1,4 @@ "use client" - import { CheckMark } from "@/components/CheckMark" import { LayoutContainer } from "@/components/Layout" import { ProfileAvatar } from "@/components/ProfileAvatar" @@ -9,15 +8,14 @@ import { Card } from "@/components/ui/Card" import { Pencil } from "@phosphor-icons/react" import { ProfileOwnerGuard } from "../_components/ProfileOwnerGuard" import { useProfile } from "../_hooks/useProfile" -import { useReferredUsers } from "../_hooks/useReferredUsers" import { EditProfile } from "./EditProfile/EditProfile" import { ProfileHeroSkeleton } from "./ProfileSkeleton" +import { ProfileSocialCounters } from "./ProfileSocialCounters" export const ProfileHero = () => { const { data: profile } = useProfile() - const { data: referredUsers } = useReferredUsers() - if (!profile || !referredUsers) return + if (!profile) return return ( @@ -53,12 +51,7 @@ export const ProfileHero = () => {

{profile.bio}

-
-
-
{referredUsers.length}
-
Guildmates
-
-
+
) diff --git a/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx b/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx new file mode 100644 index 0000000000..8b62abe592 --- /dev/null +++ b/src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx @@ -0,0 +1,87 @@ +import FarcasterImage from "@/../static/socialIcons/farcaster.svg" +import { AvatarGroup } from "@/components/ui/AvatarGroup" +import { Separator } from "@/components/ui/Separator" +import { cn } from "@/lib/utils" +import { PropsWithChildren } from "react" +import { + useFarcasterProfile, + useRelevantFarcasterFollowers, +} from "../_hooks/useFarcasterProfile" +import { useProfile } from "../_hooks/useProfile" +import { useReferredUsers } from "../_hooks/useReferredUsers" + +export const ProfileSocialCounters = ({ className }: any) => { + const { data: referredUsers } = useReferredUsers() + const { data: profile } = useProfile() + const { farcasterProfile } = useFarcasterProfile(profile?.userId) + const { relevantFollowers } = useRelevantFarcasterFollowers(farcasterProfile?.fid) + + return ( +
+ {referredUsers && ( + Guildmates + )} + {farcasterProfile && ( + <> + + + + Following + + + + {relevantFollowers?.length ? ( + + ) : ( + + + Followers + + )} + + )} +
+ ) +} + +const SocialCountTile = ({ count, children }: PropsWithChildren<{ count: any }>) => ( +
+
{count}
+
{children}
+
+) + +const RelevantFollowers = ({ + relevantFollowers, + followerCount, +}: { relevantFollowers: any; followerCount: number }) => { + const [firstFc, secondFc] = relevantFollowers + + return ( +
+ pfp_url)} + count={followerCount} + /> +
+ Followed by {firstFc.display_name} + {secondFc && ( + <> + ", " + {secondFc.display_name} + + )}{" "} + and {followerCount - Math.min(2, relevantFollowers.length)} others on + Farcaster +
+
+ ) +} diff --git a/src/app/(marketing)/profile/_hooks/useFarcasterProfile.tsx b/src/app/(marketing)/profile/_hooks/useFarcasterProfile.tsx new file mode 100644 index 0000000000..8c1478061b --- /dev/null +++ b/src/app/(marketing)/profile/_hooks/useFarcasterProfile.tsx @@ -0,0 +1,35 @@ +import { FarcasterProfile } from "@guildxyz/types" +import useUser from "components/[guild]/hooks/useUser" +import useSWRImmutable from "swr/immutable" + +export const useFarcasterProfile = (guildUserId?: number) => { + const linkedFcProfile = useSWRImmutable( + guildUserId ? `/v2/users/${guildUserId}/farcaster-profiles` : null + ).data?.at(0) + + // API reference: https://docs.neynar.com/reference/user-bulk + const { data, ...rest } = useSWRImmutable( + linkedFcProfile + ? `https://api.neynar.com/v2/farcaster/user/bulk?api_key=NEYNAR_API_DOCS&fids=${linkedFcProfile.fid}` + : null + ) + return { farcasterProfile: data?.users.at(0), ...rest } +} + +export const useRelevantFarcasterFollowers = (farcasterId?: number) => { + const currentUser = useUser() + const currentUserFcProfile = currentUser.farcasterProfiles?.at(0) + + // API reference: https://docs.neynar.com/reference/relevant-followers + const { data, ...rest } = useSWRImmutable( + farcasterId && currentUserFcProfile + ? `https://api.neynar.com/v2/farcaster/followers/relevant?api_key=NEYNAR_API_DOCS&target_fid=${farcasterId}&viewer_fid=${currentUserFcProfile.fid}` + : null + ) + return { + relevantFollowers: data?.top_relevant_followers_hydrated?.map( + ({ user }) => user + ), + ...rest, + } +}