Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add farcaster follower and relevant follower integration to /profile #1455

Merged
merged 13 commits into from
Sep 3, 2024
Merged
57 changes: 42 additions & 15 deletions src/app/(marketing)/profile/[username]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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<FarcasterProfile[]>(
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
Expand Down Expand Up @@ -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("#")

Expand Down
13 changes: 3 additions & 10 deletions src/app/(marketing)/profile/_components/ProfileHero.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client"

import { CheckMark } from "@/components/CheckMark"
import { LayoutContainer } from "@/components/Layout"
import { ProfileAvatar } from "@/components/ProfileAvatar"
Expand All @@ -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 <ProfileHeroSkeleton />
if (!profile) return <ProfileHeroSkeleton />

return (
<LayoutContainer>
Expand Down Expand Up @@ -53,12 +51,7 @@ export const ProfileHero = () => {
<p className="mt-4 max-w-md text-pretty text-center text-lg text-muted-foreground md:mt-6">
{profile.bio}
</p>
<div className="mt-8 grid grid-cols-[repeat(3,auto)] gap-y-4 space-x-6 sm:grid-cols-[repeat(5,auto)]">
<div className="flex flex-col items-center leading-tight">
<div className="font-bold md:text-lg">{referredUsers.length}</div>
<div className="text-muted-foreground">Guildmates</div>
</div>
</div>
<ProfileSocialCounters className="mt-8" />
</div>
</LayoutContainer>
)
Expand Down
87 changes: 87 additions & 0 deletions src/app/(marketing)/profile/_components/ProfileSocialCounters.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div
className={cn(
"flex flex-wrap items-center justify-center gap-6 gap-y-4 sm:flex-nowrap",
className
)}
>
{referredUsers && (
<SocialCountTile count={referredUsers.length}>Guildmates</SocialCountTile>
)}
{farcasterProfile && (
<>
<Separator orientation="vertical" className="h-10 md:h-12" />
<SocialCountTile count={farcasterProfile.following_count}>
<FarcasterImage />
Following
</SocialCountTile>

<Separator orientation="vertical" className="h-10 max-sm:hidden md:h-12" />
{relevantFollowers?.length ? (
<RelevantFollowers
relevantFollowers={relevantFollowers}
followerCount={farcasterProfile.follower_count}
/>
) : (
<SocialCountTile count={farcasterProfile.follower_count}>
<FarcasterImage />
Followers
</SocialCountTile>
)}
</>
)}
</div>
)
}

const SocialCountTile = ({ count, children }: PropsWithChildren<{ count: any }>) => (
<div className="flex flex-col items-center leading-tight">
<div className="font-bold md:text-lg">{count}</div>
<div className="flex items-center gap-1 text-muted-foreground">{children}</div>
</div>
)

const RelevantFollowers = ({
relevantFollowers,
followerCount,
}: { relevantFollowers: any; followerCount: number }) => {
const [firstFc, secondFc] = relevantFollowers

return (
<div className="flex items-center gap-2">
<AvatarGroup
imageUrls={relevantFollowers.map(({ pfp_url }) => pfp_url)}
count={followerCount}
/>
<div className="max-w-64 text-muted-foreground leading-tight">
Followed by <span className="font-bold">{firstFc.display_name}</span>
{secondFc && (
<>
<span>", "</span>
<span className="font-bold">{secondFc.display_name}</span>
</>
)}{" "}
and {followerCount - Math.min(2, relevantFollowers.length)} others on
Farcaster
</div>
</div>
)
}
35 changes: 35 additions & 0 deletions src/app/(marketing)/profile/_hooks/useFarcasterProfile.tsx
Original file line number Diff line number Diff line change
@@ -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<FarcasterProfile[]>(
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,
}
}
Loading