Skip to content

Commit

Permalink
remove airstack, create custom airstack gql fetches, fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
encryptedDegen committed Dec 7, 2024
1 parent 1b6066e commit 50d69a7
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 92 deletions.
1 change: 1 addition & 0 deletions .million/store.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"encodings":[],"reactData":{},"unusedFiles":[],"mtime":null}
Binary file modified bun.lockb
Binary file not shown.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"preinstall": "npm_config_yes=true npm exec --no-package-lock=true only-allow@latest bun"
},
"dependencies": {
"@airstack/airstack-react": "^0.6.4",
"@million/lint": "^1.0.13",
"@rainbow-me/rainbowkit": "^2.2.1",
"@react-spring/web": "^9.7.5",
Expand Down
63 changes: 63 additions & 0 deletions src/api/airstack/followings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { Address } from 'viem'
import type { AirstackFollowingsResponse } from '#/types/requests'

export const fetchAirstackFollowings = async ({
profileAddress,
platform,
pageParam
}: {
profileAddress: Address
platform: string
pageParam?: string
}) => {
try {
const followingsQuery = `
query FollowingsQuery ($platform: SocialDappName, $cursor: String) {
SocialFollowings(
input: {filter: {dappName: {_eq: $platform}, identity: {_eq: "${profileAddress}"}}, blockchain: ALL, limit: 200, cursor: $cursor}
) {
Following {
followingAddress {
addresses
primaryDomain {
name
}
}
}
pageInfo {
nextCursor
hasPrevPage
hasNextPage
}
}
}
`

const response = await fetch(`https://api.airstack.xyz/gql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: process.env.NEXT_PUBLIC_AIRSTACK_API_KEY
} as HeadersInit,
body: JSON.stringify({
query: followingsQuery,
variables: { platform, cursor: pageParam },
operationName: 'FollowingsQuery'
})
})

const json = (await response.json()) as AirstackFollowingsResponse
return {
followings: json.data.SocialFollowings,
nextPageParam: json.data.SocialFollowings.pageInfo.nextCursor,
hasNextPage: json.data.SocialFollowings.pageInfo.hasNextPage,
hasPrevPage: json.data.SocialFollowings.pageInfo.hasPrevPage
}
} catch (error) {
return {
followings: null,
nextPageParam: undefined,
hasNextPage: false
}
}
}
37 changes: 37 additions & 0 deletions src/api/airstack/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { AirstackProfileResponse } from '#/types/requests'

export const fetchAirstackProfile = async (platform: string, handle: string) => {
// Limit is set to 1 since we allow only full name search that returns only one profile
const profileQuery = `
query ProfileQuery ($platform: SocialDappName) {
Socials(
input: {filter: {dappName: {_eq: $platform}, profileName: {_eq: "${handle.replace('@', '')}"}}, blockchain: ethereum, limit: 1}
) {
Social {
profileImage
profileHandle
profileName
userAddress
}
}
}
`

const response = await fetch(`https://api.airstack.xyz/gql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: process.env.NEXT_PUBLIC_AIRSTACK_API_KEY
} as HeadersInit,
body: JSON.stringify({
query: profileQuery,
variables: { platform },
operationName: 'ProfileQuery'
})
})

const json = (await response.json()) as AirstackProfileResponse

// return the first social profile since there is only one
return json.data.Socials.Social[0]
}
149 changes: 66 additions & 83 deletions src/app/cart/hooks/use-import-modal.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import type { Address } from 'viem'
import { useEffect, useMemo, useState } from 'react'
import { init, useQuery, useQueryWithPagination } from '@airstack/airstack-react'
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'

import { SECOND } from '#/lib/constants'
import { useCart } from '#/contexts/cart-context'
import { listOpAddListRecord } from '#/utils/list-ops'
import type { ImportPlatformType } from '#/types/common'
import type { AirstackFollowings } from '#/types/requests'
import { fetchAirstackProfile } from '#/api/airstack/profile'
import { useEFPProfile } from '#/contexts/efp-profile-context'
import { fetchAirstackFollowings } from '#/api/airstack/followings'

init('0366bbe276e04996af5f92ebb7899f19', { env: 'dev', cache: true })

export type importFollowingType = {
export type ImportFollowingType = {
address: Address
domains: { name: string }[]
primaryDomain: string
}

const useImportModal = (platform: ImportPlatformType) => {
const [handle, setHandle] = useState('')
const [currHandle, setCurrHandle] = useState('')

const [followings, setFollowings] = useState<importFollowingType[]>([])
const [allFollowings, setAllFollowings] = useState<importFollowingType[]>([])
const [followings, setFollowings] = useState<ImportFollowingType[]>([])
const [allFollowings, setAllFollowings] = useState<ImportFollowingType[]>([])
const [onlyImportWithEns, setOnlyImportWithEns] = useState(true)
const [isFollowingsLoading, setIsFollowingsLoading] = useState(false)

Expand All @@ -36,96 +37,78 @@ const useImportModal = (platform: ImportPlatformType) => {
return () => clearTimeout(inputTimeout)
}, [currHandle])

// Fetch profile from Airstack
const profileQuery = `
query ProfileQuery ($platform: SocialDappName) {
Socials(
input: {filter: {dappName: {_eq: $platform}, profileName: {_eq: "${handle.replace('@', '')}"}}, blockchain: ethereum, limit: 1}
) {
Social {
profileImage
profileHandle
profileName
userAddress
}
}
}
`

const { data: fetchedProfile, loading: isSocialProfileLoading } = useQuery(profileQuery, {
platform
const { data: fetchedProfile, isFetching: isSocialProfileLoading } = useQuery({
queryKey: ['profile', platform, handle],
queryFn: async () => await fetchAirstackProfile(platform, handle),
enabled: !!handle
})
const socialProfile =
fetchedProfile && !!fetchedProfile?.Socials?.Social
? {
...fetchedProfile?.Socials?.Social?.[0],
profileImage: fetchedProfile?.Socials?.Social?.[0]?.profileImage?.includes('ipfs://')
? `https://gateway.pinata.cloud/ipfs/${fetchedProfile?.Socials?.Social?.[0]?.profileImage.replace(
'ipfs://',
''
)}`
: fetchedProfile?.Socials?.Social?.[0]?.profileImage
}
: null

// Fetch followings from Airstack
const followingsQuery = useMemo(
() => `
query FollowingsQuery ($platform: SocialDappName) {
SocialFollowings(
input: {filter: {dappName: {_eq: $platform}, identity: {_eq: "${socialProfile?.userAddress}"}}, blockchain: ALL, limit: 200}
) {
Following {
followingAddress {
addresses
primaryDomain {
name
}
}

// replace ipfs with pinata gateway (pinata currently most stable for me https://ipfs.github.io/public-gateway-checker/)
const socialProfile = fetchedProfile
? {
...fetchedProfile,
profileImage: fetchedProfile?.profileImage?.includes('ipfs://')
? `https://ipfs.io/ipfs/${fetchedProfile?.profileImage.replace('ipfs://', '')}`
: fetchedProfile?.profileImage
}
}
}
`,
[socialProfile]
)
: null

const {
data: fetchedFollowings,
loading: isFetchedFollowingsLoading,
pagination: { hasNextPage, getNextPage, hasPrevPage }
} = useQueryWithPagination(followingsQuery, { platform })
isLoading: isFetchedFollowingsLoading,
hasNextPage: hasNextPageFollowings,
hasPreviousPage: hasPreviousPageFollowings,
fetchNextPage: fetchNextPageFollowings
} = useInfiniteQuery({
queryKey: ['followings', platform, handle, socialProfile?.userAddress],
queryFn: async ({ pageParam }) => {
if (!socialProfile?.userAddress)
return { followings: null, nextPageParam: undefined, hasNextPage: false }

return await fetchAirstackFollowings({
profileAddress: socialProfile.userAddress as Address,
platform,
pageParam
})
},
initialPageParam: '',
getNextPageParam: lastPage => (lastPage?.hasNextPage ? lastPage?.nextPageParam : undefined)
})

const reducedFollowings = useMemo(
() =>
fetchedFollowings?.pages.reduce<AirstackFollowings[]>((acc, page) => {
if (page?.followings?.Following) acc.push(...page.followings.Following)
return acc
}, [] as AirstackFollowings[]),
[fetchedFollowings]
)

useEffect(() => {
if (currHandle !== handle) return
if (!hasPrevPage) setFollowings([])
if (hasNextPage) getNextPage()

if (
fetchedFollowings?.SocialFollowings?.Following &&
fetchedFollowings?.SocialFollowings?.Following.length > 0
) {
if (currHandle !== handle || isFetchedFollowingsLoading) return
if (!hasPreviousPageFollowings) setFollowings([])

if (reducedFollowings && reducedFollowings.length > 0) {
if (hasNextPageFollowings) fetchNextPageFollowings()
setIsFollowingsLoading(true)

const newFollowingAddresses = fetchedFollowings?.SocialFollowings?.Following.map(
(following: any) => ({
address: following.followingAddress.addresses?.[0],
primaryDomain: following.followingAddress?.primaryDomain?.name
})
)
const followingAddresses = reducedFollowings.map((following: any) => ({
address: following.followingAddress.addresses?.[0],
primaryDomain: following.followingAddress?.primaryDomain?.name
}))

const filteredNewFollowingAddresses = newFollowingAddresses.filter((following: any) =>
const filteredFollowingAddresses = followingAddresses.filter((following: any) =>
onlyImportWithEns ? !!following.primaryDomain : true
)

setAllFollowings(currFollowings => [
...new Set([...currFollowings, ...newFollowingAddresses])
])
setFollowings(currFollowings => [
...new Set([...currFollowings, ...filteredNewFollowingAddresses])
])
if (!hasNextPageFollowings) {
setAllFollowings(followingAddresses)
setFollowings(filteredFollowingAddresses)
}
}
if (!hasNextPage) setIsFollowingsLoading(false)
}, [fetchedFollowings])

if (!hasNextPageFollowings) setIsFollowingsLoading(false)
}, [reducedFollowings])

useEffect(() => {
if (!allFollowings || allFollowings.length === 0) return
Expand Down
4 changes: 2 additions & 2 deletions src/app/leaderboard/components/filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const Filters: React.FC<FiltersProps> = ({ filter, onSelectFilter }) => {
<div ref={clickAwayRef} className='relative w-full md:w-64 z-40 mx-auto max-w-108'>
<div
onClick={() => setIsDropdownOpen(prev => !prev)}
className='flex w-full cursor-pointer flex-wrap h-[50px] z-30 justify-between px-3 glass-card border-grey hover:border-text/80 transition-colors rounded-xl border-[3px] bg-neutral items-center gap-4'
className='flex w-full cursor-pointer flex-wrap h-[50px] z-30 justify-between px-3 border-grey hover:border-text/80 transition-colors rounded-xl border-[3px] bg-neutral items-center gap-4'
>
<div
key={filter}
Expand All @@ -42,7 +42,7 @@ const Filters: React.FC<FiltersProps> = ({ filter, onSelectFilter }) => {
</div>
<div
className={cn(
'absolute top-1/2 rounded-xl glass-card left-0 bg-neutral border-grey border-[3px] -z-10 w-full h-fit pt-5 transition-all',
'absolute top-1/2 rounded-xl left-0 bg-neutral border-grey border-[3px] -z-10 w-full h-fit pt-5 transition-all',
isDropdownOpen ? 'flex' : 'hidden pointer-events-none'
)}
>
Expand Down
4 changes: 2 additions & 2 deletions src/app/leaderboard/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ const LeaderboardTable = () => {
</div>
<div className='flex justify-between gap-2'>
<div className='relative w-full sm:w-[260px] 2xl:w-[300px]'>
<div className='rounded-xl w-full group glass-card overflow-hidden border-[3px] border-grey sm:text-sm focus:border-text/80 hover:border-text/80 focus-within:border-text/80 transition-colors'>
<div className='rounded-xl w-full group overflow-hidden border-[3px] border-grey sm:text-sm focus:border-text/80 hover:border-text/80 focus-within:border-text/80 transition-colors'>
<div
className='pointer-events-none absolute inset-y-0 right-0 flex items-center pl-3'
aria-hidden='true'
Expand All @@ -115,7 +115,7 @@ const LeaderboardTable = () => {
placeholder={t('search placeholder')}
value={currentSearch}
onChange={handleSearchEvent}
className='h-[44px] block w-full border-0 font-medium border-transparent pl-4 pr-10 sm:text-sm bg-neutral/70'
className='h-[44px] block w-full border-0 font-medium border-transparent pl-4 pr-10 sm:text-sm bg-neutral'
/>
</div>
</div>
Expand Down
5 changes: 2 additions & 3 deletions src/components/checkout/select-chain-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@ export function SelectChainCard({
setNewListAsPrimary: boolean
setSetNewListAsPrimary: (state: boolean) => void
}) {
const { t } = useTranslation()
const { lists } = useEFPProfile()
const currentChainId = useChainId()
const { switchChain } = useSwitchChain()

const { lists } = useEFPProfile()
const { t } = useTranslation()

return (
<>
<div className='flex flex-col gap-2'>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/wagmi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { http, fallback, createStorage, cookieStorage, createConfig } from 'wagm

import { APP_DESCRIPTION, APP_NAME, APP_URL } from '#/lib/constants'

coinbaseWallet.preference = 'smartWalletOnly'
coinbaseWallet.preference = 'all'

// Define the connectors for the app
// Purposely using only these for now because of a localStorage error with the Coinbase Wallet connector
Expand Down
27 changes: 27 additions & 0 deletions src/types/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,3 +267,30 @@ export type RecommendedProfilesResponseType = {
}

export type QRCodeResponse = StaticImageData

// Airstack
export type AirstackProfileResponse = {
data: {
Socials: {
Social: {
profileImage: string
profileHandle: string
profileName: string
userAddress: string
}[]
}
}
}

export type AirstackFollowings = {
followingAddress: { addresses: Address[]; primaryDomain: { name: string } }
}

export type AirstackFollowingsResponse = {
data: {
SocialFollowings: {
Following: AirstackFollowings[]
pageInfo: { nextCursor: string; hasPrevPage: boolean; hasNextPage: boolean }
}
}
}

0 comments on commit 50d69a7

Please sign in to comment.