Skip to content

Commit

Permalink
Add farcaster follower and relevant follower integration to /profile (
Browse files Browse the repository at this point in the history
#1455)

* chore: try adding farcaster integration

* feat: refine ProfileHero

* chore: put revalidate tag on fc profile

* refactor: move logic to ProfileSocialCounters component

* refactor: SocialCountTile

* refactor: rename variables and add docs, fallback if referred is not defined

* refactor: farcaster hooks

* UI: remove divider on small screens

* adjust referredUsers condition

---------

Co-authored-by: valid <[email protected]>
  • Loading branch information
dominik-stumpf and dovalid authored Sep 3, 2024
1 parent 4bf7eba commit e8174d2
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 25 deletions.
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 @@ -107,26 +134,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,
}
}

0 comments on commit e8174d2

Please sign in to comment.