Skip to content
Open
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
10 changes: 8 additions & 2 deletions src/app/profile/[user]/components/main-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { fetchAccount } from 'ethereum-identity-kit'
import { useIsClient, useWindowSize } from 'ethereum-identity-kit'
import ActivityPanel from './activity'
import BrokerPanel from './brokerPanel'
import PrivateForMePanel from './privateForMePanel'
import { useQuery } from '@tanstack/react-query'
import OfferPanel from './offerPanel'
import { changeTab, selectUserProfile, setLastVisitedProfile } from '@/state/reducers/portfolio/profile'
Expand Down Expand Up @@ -95,9 +96,9 @@ const MainPanel: React.FC<Props> = ({ user }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user])

// ensure that only the owner of the profile can see the watchlist
// ensure that only the owner of the profile can see the watchlist and private_for_me tabs
useEffect(() => {
if (profileTab === 'watchlist') {
if (profileTab === 'watchlist' || profileTab === 'private_for_me') {
if (
!(
userAddress &&
Expand All @@ -119,6 +120,7 @@ const MainPanel: React.FC<Props> = ({ user }) => {
const showOfferPanel = profileTab === 'sent_offers' || profileTab === 'received_offers'
const showActivityPanel = profileTab === 'activity'
const showBrokerPanel = profileTab === 'broker'
const showPrivateForMePanel = profileTab === 'private_for_me'

return (
<Suspense>
Expand All @@ -140,6 +142,7 @@ const MainPanel: React.FC<Props> = ({ user }) => {
showOfferPanel={showOfferPanel}
showActivityPanel={showActivityPanel}
showBrokerPanel={showBrokerPanel}
showPrivateForMePanel={showPrivateForMePanel}
isMyProfile={isMyProfile}
/>
</div>
Expand All @@ -159,6 +162,7 @@ interface ProfileContentProps {
showOfferPanel: boolean
showActivityPanel: boolean
showBrokerPanel: boolean
showPrivateForMePanel: boolean
isMyProfile: boolean
}

Expand All @@ -168,6 +172,7 @@ const ProfileContent: React.FC<ProfileContentProps> = ({
showOfferPanel,
showActivityPanel,
showBrokerPanel,
showPrivateForMePanel,
isMyProfile,
}) => {
const isClient = useIsClient()
Expand All @@ -187,6 +192,7 @@ const ProfileContent: React.FC<ProfileContentProps> = ({
{showOfferPanel && <OfferPanel user={userAddress} />}
{showActivityPanel && <ActivityPanel user={userAddress} />}
{showBrokerPanel && <BrokerPanel user={userAddress} />}
{showPrivateForMePanel && <PrivateForMePanel user={userAddress} />}
</div>
)
}
Expand Down
67 changes: 67 additions & 0 deletions src/app/profile/[user]/components/privateForMePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import React from 'react'
import { Address } from 'viem'
import Domains from '@/components/domains'
import ViewSelector from '@/components/domains/viewSelector'
import { usePrivateListings } from '../hooks/usePrivateListings'
import { PORTFOLIO_PRIVATE_FOR_ME_DISPLAYED_COLUMNS } from '@/constants/domains/marketplaceDomains'
import { cn } from '@/utils/tailwind'
import { useNavbar } from '@/context/navbar'
import { useFilterRouter } from '@/hooks/filters/useFilterRouter'

interface Props {
user: Address | undefined
}

const PrivateForMePanel: React.FC<Props> = ({ user }) => {
const { isNavbarVisible } = useNavbar()
const { selectors } = useFilterRouter()

const {
privateListings,
isPrivateListingsLoading,
fetchMorePrivateListings,
hasMorePrivateListings,
totalPrivateListings,
} = usePrivateListings(user)

return (
<div className='z-0 flex w-full flex-col'>
<div
className={cn(
'py-md md:py-lg px-sm md:px-md lg:px-lg transition-top bg-background sticky z-50 flex w-full flex-col items-center justify-between gap-2 duration-300 sm:flex-row',
isNavbarVisible ? 'top-26 md:top-32' : 'top-12 md:top-14'
)}
>
<div className='px-sm flex w-full items-center gap-3 sm:w-fit md:p-0'>
<p className='text-neutral text-lg font-semibold'>
{totalPrivateListings} private {totalPrivateListings === 1 ? 'listing' : 'listings'} for you
</p>
</div>
<div className='px-sm flex w-full items-center justify-end gap-2 sm:w-fit'>
<ViewSelector />
</div>
</div>
<Domains
domains={privateListings}
loadingRowCount={20}
filtersOpen={selectors.filters.open}
paddingBottom='160px'
noResults={!isPrivateListingsLoading && privateListings.length === 0}
isLoading={isPrivateListingsLoading}
hasMoreDomains={hasMorePrivateListings}
fetchMoreDomains={() => {
if (hasMorePrivateListings && !isPrivateListingsLoading) {
fetchMorePrivateListings()
}
}}
displayedDetails={PORTFOLIO_PRIVATE_FOR_ME_DISPLAYED_COLUMNS}
showWatchlist={false}
isBulkSelecting={false}
/>
</div>
)
}

export default PrivateForMePanel
16 changes: 16 additions & 0 deletions src/app/profile/[user]/components/tabSwitcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useUserContext } from '@/context/user'
import { useOffers } from '../hooks/useOffers'
import { useDomains } from '../hooks/useDomains'
import { useBrokeredListings } from '../hooks/useBrokeredListings'
import { usePrivateListings } from '../hooks/usePrivateListings'
import { useNavbar } from '@/context/navbar'
import { formatTotalTabItems } from '@/utils/formatTabItems'
import { useFilterRouter } from '@/hooks/filters/useFilterRouter'
Expand All @@ -28,6 +29,7 @@ const TabSwitcher: React.FC<TabSwitcherProps> = ({ user }) => {
useDomains(user)
const { totalReceivedOffers, totalSentOffers } = useOffers(user)
const { totalActiveBrokeredListings, totalBrokeredListings } = useBrokeredListings(user)
const { totalPrivateListings } = usePrivateListings(user)
const { isNavbarVisible } = useNavbar()
const { actions } = useFilterRouter()

Expand All @@ -52,6 +54,16 @@ const TabSwitcher: React.FC<TabSwitcherProps> = ({ user }) => {
if (tab.value === 'watchlist') {
return user && userAddress && user.toLowerCase() === userAddress.toLowerCase() && authStatus === 'authenticated'
}
// Only show private_for_me tab to profile owner and if there are private listings
if (tab.value === 'private_for_me') {
return (
user &&
userAddress &&
user.toLowerCase() === userAddress.toLowerCase() &&
authStatus === 'authenticated' &&
totalPrivateListings > 0
)
}
// Only show broker tab if there are brokered listings
if (tab.value === 'broker') {
return totalBrokeredListings > 0
Expand Down Expand Up @@ -89,6 +101,7 @@ const TabSwitcher: React.FC<TabSwitcherProps> = ({ user }) => {
totalExpiredDomains,
totalReceivedOffers,
totalSentOffers,
totalPrivateListings,
])

const getTotalItems = useMemo(
Expand All @@ -110,6 +123,8 @@ const TabSwitcher: React.FC<TabSwitcherProps> = ({ user }) => {
return formatTotalTabItems(totalSentOffers)
case 'broker':
return formatTotalTabItems(totalActiveBrokeredListings || 0)
case 'private_for_me':
return formatTotalTabItems(totalPrivateListings)
case 'activity':
return 0
}
Expand All @@ -123,6 +138,7 @@ const TabSwitcher: React.FC<TabSwitcherProps> = ({ user }) => {
totalGraceDomains,
totalExpiredDomains,
totalActiveBrokeredListings,
totalPrivateListings,
]
)

Expand Down
137 changes: 137 additions & 0 deletions src/app/profile/[user]/hooks/usePrivateListings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Address } from 'viem'
import { useInfiniteQuery } from '@tanstack/react-query'
import { useUserContext } from '@/context/user'
import { authFetch } from '@/api/authFetch'
import { DEFAULT_FETCH_LIMIT } from '@/constants/api'
import { MarketplaceDomainType, DomainListingType } from '@/types/domains'

interface PrivateListingApiResponse {
id: number
ens_name: string
ens_name_id: number
token_id: string
seller_address: string
price_wei: string
currency_address: string
status: string
created_at: string
expires_at: string | null
name_expiry_date: string | null
current_owner: string
order_hash: string
order_data: any
source: string
}

interface PrivateListingsResponse {
success: boolean
data: {
listings: PrivateListingApiResponse[]
pagination: {
page: number
limit: number
total: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
}
}

// Transform API response to MarketplaceDomainType format
const transformToMarketplaceDomain = (listing: PrivateListingApiResponse): MarketplaceDomainType => {
const domainListing: DomainListingType = {
id: listing.id,
price: listing.price_wei,
price_wei: listing.price_wei,
currency_address: listing.currency_address as Address,
status: listing.status,
seller_address: listing.seller_address,
order_hash: listing.order_hash,
order_data: listing.order_data,
expires_at: listing.expires_at || '',
created_at: listing.created_at,
source: listing.source || 'grails',
broker_address: null,
broker_fee_bps: null,
}

return {
id: listing.ens_name_id || listing.id,
name: listing.ens_name,
token_id: listing.token_id,
owner: listing.current_owner as Address,
expiry_date: listing.name_expiry_date,
registration_date: null,
metadata: {},
has_numbers: /\d/.test(listing.ens_name),
has_emoji: false,
clubs: [],
listings: [domainListing],
highest_offer_wei: null,
highest_offer_id: null,
highest_offer_currency: null,
offer: null,
last_sale_price: null,
last_sale_price_usd: null,
last_sale_currency: null,
last_sale_date: null,
view_count: 0,
watchers_count: 0,
downvotes: 0,
upvotes: 0,
watchlist_record_id: null,
}
}

export const usePrivateListings = (user: Address | undefined) => {
const { userAddress, authStatus } = useUserContext()

const isMyProfile =
!!user && !!userAddress && user.toLowerCase() === userAddress.toLowerCase() && authStatus === 'authenticated'

const { data, isLoading, isFetchingNextPage, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ['profile', 'private_listings', userAddress],
queryFn: async ({ pageParam = 1 }) => {
if (!isMyProfile) {
return {
domains: [] as MarketplaceDomainType[],
total: 0,
nextPage: null as number | null,
}
}

const response = await authFetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/v1/listings/private-for-me?page=${pageParam}&limit=${DEFAULT_FETCH_LIMIT}`
)

if (!response.ok) {
throw new Error('Failed to fetch private listings')
}

const data: PrivateListingsResponse = await response.json()

return {
domains: data.data.listings.map(transformToMarketplaceDomain),
total: data.data.pagination.total,
nextPage: data.data.pagination.hasNext ? pageParam + 1 : null,
}
},
getNextPageParam: (lastPage) => lastPage.nextPage,
initialPageParam: 1,
enabled: isMyProfile,
staleTime: 30000,
})

const privateListings = data?.pages?.flatMap((page) => page.domains) ?? []
const totalPrivateListings = data?.pages?.[0]?.total ?? 0

return {
privateListings,
isPrivateListingsLoading: isLoading,
isPrivateListingsFetchingNextPage: isFetchingNextPage,
fetchMorePrivateListings: fetchNextPage,
hasMorePrivateListings: !!hasNextPage,
totalPrivateListings,
}
}
Loading