|
@@ -4,55 +4,51 @@ import { useSearchParams } from 'react-router-dom'
|
|
|
|
|
|
import { useMemberships } from '@/api/hooks/membership'
|
|
import { useMemberships } from '@/api/hooks/membership'
|
|
import { NftActivityOrderByInput, OwnedNftOrderByInput } from '@/api/queries/__generated__/baseTypes.generated'
|
|
import { NftActivityOrderByInput, OwnedNftOrderByInput } from '@/api/queries/__generated__/baseTypes.generated'
|
|
-import { SvgActionFilters } from '@/assets/icons'
|
|
|
|
|
|
+import { FallbackContainer } from '@/components/AllNftSection'
|
|
import { EmptyFallback } from '@/components/EmptyFallback'
|
|
import { EmptyFallback } from '@/components/EmptyFallback'
|
|
-import { FiltersBar, useFiltersBar } from '@/components/FiltersBar'
|
|
|
|
import { LimitedWidthContainer } from '@/components/LimitedWidthContainer'
|
|
import { LimitedWidthContainer } from '@/components/LimitedWidthContainer'
|
|
|
|
+import { Section, SectionProps } from '@/components/Section/Section'
|
|
import { ViewErrorFallback } from '@/components/ViewErrorFallback'
|
|
import { ViewErrorFallback } from '@/components/ViewErrorFallback'
|
|
import { ViewWrapper } from '@/components/ViewWrapper'
|
|
import { ViewWrapper } from '@/components/ViewWrapper'
|
|
import { Button } from '@/components/_buttons/Button'
|
|
import { Button } from '@/components/_buttons/Button'
|
|
-import { Select } from '@/components/_inputs/Select'
|
|
|
|
|
|
+import { NftTileViewer } from '@/components/_nft/NftTileViewer'
|
|
import { MemberTabs, QUERY_PARAMS, absoluteRoutes } from '@/config/routes'
|
|
import { MemberTabs, QUERY_PARAMS, absoluteRoutes } from '@/config/routes'
|
|
-import { NFT_SORT_ACTIVITY_OPTIONS, NFT_SORT_OPTIONS } from '@/config/sorting'
|
|
|
|
import { useHeadTags } from '@/hooks/useHeadTags'
|
|
import { useHeadTags } from '@/hooks/useHeadTags'
|
|
|
|
+import { useInfiniteNftsGrid } from '@/hooks/useInfiniteNftsGrid'
|
|
|
|
+import { SORTING_FILTERS, useNftSectionFilters } from '@/hooks/useNftSectionFilters'
|
|
import { getMemberAvatar } from '@/providers/assets/assets.helpers'
|
|
import { getMemberAvatar } from '@/providers/assets/assets.helpers'
|
|
import { useUser } from '@/providers/user/user.hooks'
|
|
import { useUser } from '@/providers/user/user.hooks'
|
|
|
|
+import { InfiniteLoadingOffsets } from '@/utils/loading.contants'
|
|
import { SentryLogger } from '@/utils/logs'
|
|
import { SentryLogger } from '@/utils/logs'
|
|
|
|
|
|
import { MemberAbout } from './MemberAbout'
|
|
import { MemberAbout } from './MemberAbout'
|
|
import { MemberActivity } from './MemberActivity'
|
|
import { MemberActivity } from './MemberActivity'
|
|
-import { MemberNFTs } from './MemberNFTs'
|
|
|
|
-import {
|
|
|
|
- FilterButtonContainer,
|
|
|
|
- NotFoundMemberContainer,
|
|
|
|
- SortContainer,
|
|
|
|
- StyledMembershipInfo,
|
|
|
|
- StyledTabs,
|
|
|
|
- TabsContainer,
|
|
|
|
- TabsWrapper,
|
|
|
|
-} from './MemberView.styles'
|
|
|
|
-
|
|
|
|
-const TABS: MemberTabs[] = ['NFTs owned', 'Activity', 'About']
|
|
|
|
|
|
+import { NotFoundMemberContainer, StyledMembershipInfo } from './MemberView.styles'
|
|
|
|
+
|
|
|
|
+const TABS: MemberTabs[] = ['NFTs', 'Activity', 'About']
|
|
|
|
+
|
|
|
|
+const ACTIVITY_SORTING_FILTERS = [
|
|
|
|
+ {
|
|
|
|
+ label: 'Newest',
|
|
|
|
+ value: NftActivityOrderByInput.EventTimestampDesc,
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ label: 'Oldest',
|
|
|
|
+ value: NftActivityOrderByInput.EventTimestampAsc,
|
|
|
|
+ },
|
|
|
|
+]
|
|
|
|
|
|
export const MemberView: FC = () => {
|
|
export const MemberView: FC = () => {
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const [searchParams, setSearchParams] = useSearchParams()
|
|
const currentTabName = searchParams.get(QUERY_PARAMS.TAB) as MemberTabs | null
|
|
const currentTabName = searchParams.get(QUERY_PARAMS.TAB) as MemberTabs | null
|
|
- const [sortBy, setSortBy] = useState<OwnedNftOrderByInput>(OwnedNftOrderByInput.CreatedAtDesc)
|
|
|
|
const [sortByTimestamp, setSortByTimestamp] = useState<NftActivityOrderByInput>(
|
|
const [sortByTimestamp, setSortByTimestamp] = useState<NftActivityOrderByInput>(
|
|
NftActivityOrderByInput.EventTimestampDesc
|
|
NftActivityOrderByInput.EventTimestampDesc
|
|
)
|
|
)
|
|
const navigate = useNavigate()
|
|
const navigate = useNavigate()
|
|
- const [currentTab, setCurrentTab] = useState<MemberTabs | null>(null)
|
|
|
|
- const [nftCount, setNftCount] = useState<number | undefined>()
|
|
|
|
- const { memberId, activeMembership } = useUser()
|
|
|
|
|
|
+ const [currentTab, setCurrentTab] = useState<typeof TABS[number] | null>(null)
|
|
|
|
+ const { memberId } = useUser()
|
|
const { handle } = useParams()
|
|
const { handle } = useParams()
|
|
const headTags = useHeadTags(handle)
|
|
const headTags = useHeadTags(handle)
|
|
- const filtersBarLogic = useFiltersBar()
|
|
|
|
- const {
|
|
|
|
- ownedNftWhereInput,
|
|
|
|
- filters: { setIsFiltersOpen, isFiltersOpen },
|
|
|
|
- canClearFilters: { canClearAllFilters },
|
|
|
|
- } = filtersBarLogic
|
|
|
|
|
|
|
|
const {
|
|
const {
|
|
memberships,
|
|
memberships,
|
|
@@ -69,55 +65,143 @@ export const MemberView: FC = () => {
|
|
const member = memberships?.find((member) => member.handle === handle)
|
|
const member = memberships?.find((member) => member.handle === handle)
|
|
const { url: avatarUrl, isLoadingAsset: avatarLoading } = getMemberAvatar(member)
|
|
const { url: avatarUrl, isLoadingAsset: avatarLoading } = getMemberAvatar(member)
|
|
|
|
|
|
- const toggleFilters = () => {
|
|
|
|
- setIsFiltersOpen((value) => !value)
|
|
|
|
- }
|
|
|
|
- const handleSorting = (value?: OwnedNftOrderByInput | null) => {
|
|
|
|
- if (value) {
|
|
|
|
- setSortBy(value)
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- const handleSortingActivity = (value?: NftActivityOrderByInput | null) => {
|
|
|
|
- if (value) {
|
|
|
|
- setSortByTimestamp(value)
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
|
|
+ const {
|
|
|
|
+ ownedNftWhereInput,
|
|
|
|
+ order,
|
|
|
|
+ hasAppliedFilters,
|
|
|
|
+ rawFilters,
|
|
|
|
+ actions: { onApplyFilters, setOrder, clearFilters },
|
|
|
|
+ } = useNftSectionFilters()
|
|
|
|
+ const { columns, fetchMore, pageInfo, tiles, totalCount } = useInfiniteNftsGrid({
|
|
|
|
+ where: {
|
|
|
|
+ AND: [
|
|
|
|
+ ownedNftWhereInput,
|
|
|
|
+ {
|
|
|
|
+ OR: [
|
|
|
|
+ {
|
|
|
|
+ owner: {
|
|
|
|
+ isTypeOf_eq: 'NftOwnerChannel',
|
|
|
|
+ channel: {
|
|
|
|
+ ownerMember: {
|
|
|
|
+ handle_eq: handle,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ owner: {
|
|
|
|
+ isTypeOf_eq: 'NftOwnerMember',
|
|
|
|
+ member: {
|
|
|
|
+ handle_eq: handle,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ ],
|
|
|
|
+ },
|
|
|
|
+ ],
|
|
|
|
+ },
|
|
|
|
+ orderBy: order,
|
|
|
|
+ })
|
|
|
|
+
|
|
const handleSetCurrentTab = async (tab: number) => {
|
|
const handleSetCurrentTab = async (tab: number) => {
|
|
navigate(absoluteRoutes.viewer.member(handle, { tab: TABS[tab] }))
|
|
navigate(absoluteRoutes.viewer.member(handle, { tab: TABS[tab] }))
|
|
}
|
|
}
|
|
|
|
|
|
const mappedTabs = TABS.map((tab) => ({
|
|
const mappedTabs = TABS.map((tab) => ({
|
|
name: tab,
|
|
name: tab,
|
|
- pillText: tab === 'NFTs owned' ? nftCount : undefined,
|
|
|
|
|
|
+ pillText: tab === 'NFTs' ? totalCount : undefined,
|
|
}))
|
|
}))
|
|
|
|
|
|
const tabContent = useMemo(() => {
|
|
const tabContent = useMemo(() => {
|
|
switch (currentTab) {
|
|
switch (currentTab) {
|
|
- case 'NFTs owned':
|
|
|
|
- return (
|
|
|
|
- <MemberNFTs
|
|
|
|
- ownedNftWhereInput={ownedNftWhereInput}
|
|
|
|
- sortBy={sortBy}
|
|
|
|
- isFiltersApplied={canClearAllFilters}
|
|
|
|
- owner={activeMembership?.handle === handle}
|
|
|
|
- setNftCount={setNftCount}
|
|
|
|
- />
|
|
|
|
- )
|
|
|
|
|
|
+ case 'NFTs':
|
|
|
|
+ return tiles?.length
|
|
|
|
+ ? tiles.map((nft, idx) => <NftTileViewer key={idx} nftId={nft.id} />)
|
|
|
|
+ : [
|
|
|
|
+ <FallbackContainer key="fallback">
|
|
|
|
+ <EmptyFallback
|
|
|
|
+ title="No NFTs found"
|
|
|
|
+ subtitle="Please, try changing your filtering criteria."
|
|
|
|
+ button={
|
|
|
|
+ hasAppliedFilters && (
|
|
|
|
+ <Button variant="secondary" onClick={() => clearFilters()}>
|
|
|
|
+ Clear all filters
|
|
|
|
+ </Button>
|
|
|
|
+ )
|
|
|
|
+ }
|
|
|
|
+ />
|
|
|
|
+ </FallbackContainer>,
|
|
|
|
+ ]
|
|
case 'Activity':
|
|
case 'Activity':
|
|
- return <MemberActivity memberId={member?.id} sort={sortByTimestamp} />
|
|
|
|
|
|
+ return [<MemberActivity key="member-activity" memberId={member?.id} sort={sortByTimestamp} />]
|
|
case 'About':
|
|
case 'About':
|
|
- return <MemberAbout />
|
|
|
|
|
|
+ return [<MemberAbout key="member-about" />]
|
|
|
|
+ default:
|
|
|
|
+ return [<div key="empty" />]
|
|
}
|
|
}
|
|
- }, [
|
|
|
|
- activeMembership?.handle,
|
|
|
|
- canClearAllFilters,
|
|
|
|
- currentTab,
|
|
|
|
- handle,
|
|
|
|
- member?.id,
|
|
|
|
- ownedNftWhereInput,
|
|
|
|
- sortBy,
|
|
|
|
- sortByTimestamp,
|
|
|
|
- ])
|
|
|
|
|
|
+ }, [clearFilters, currentTab, hasAppliedFilters, member?.id, sortByTimestamp, tiles])
|
|
|
|
+
|
|
|
|
+ const gridColumns = useMemo(() => {
|
|
|
|
+ switch (currentTab) {
|
|
|
|
+ case 'NFTs':
|
|
|
|
+ return {
|
|
|
|
+ xss: {
|
|
|
|
+ columns: 1,
|
|
|
|
+ },
|
|
|
|
+ sm: {
|
|
|
|
+ columns: 2,
|
|
|
|
+ },
|
|
|
|
+ md: {
|
|
|
|
+ columns: 3,
|
|
|
|
+ },
|
|
|
|
+ lg: {
|
|
|
|
+ columns: 4,
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ default:
|
|
|
|
+ return {
|
|
|
|
+ xss: {
|
|
|
|
+ columns: 1,
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }, [currentTab])
|
|
|
|
+
|
|
|
|
+ const headerFilters = useMemo((): Omit<
|
|
|
|
+ SectionProps<OwnedNftOrderByInput | NftActivityOrderByInput>['headerProps'],
|
|
|
|
+ 'start'
|
|
|
|
+ > => {
|
|
|
|
+ switch (currentTab) {
|
|
|
|
+ case 'NFTs':
|
|
|
|
+ return {
|
|
|
|
+ onApplyFilters,
|
|
|
|
+ filters: rawFilters,
|
|
|
|
+ sort: {
|
|
|
|
+ type: 'toggle-button',
|
|
|
|
+ toggleButtonOptionTypeProps: {
|
|
|
|
+ type: 'options',
|
|
|
|
+ options: SORTING_FILTERS,
|
|
|
|
+ value: order,
|
|
|
|
+ onChange: setOrder,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ case 'Activity':
|
|
|
|
+ return {
|
|
|
|
+ sort: {
|
|
|
|
+ type: 'toggle-button',
|
|
|
|
+ toggleButtonOptionTypeProps: {
|
|
|
|
+ type: 'options',
|
|
|
|
+ options: ACTIVITY_SORTING_FILTERS,
|
|
|
|
+ value: sortByTimestamp,
|
|
|
|
+ onChange: setSortByTimestamp,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ default:
|
|
|
|
+ return {}
|
|
|
|
+ }
|
|
|
|
+ }, [currentTab, onApplyFilters, order, rawFilters, setOrder, sortByTimestamp])
|
|
|
|
|
|
// At mount set the tab from the search params
|
|
// At mount set the tab from the search params
|
|
const initialRender = useRef(true)
|
|
const initialRender = useRef(true)
|
|
@@ -131,11 +215,9 @@ export const MemberView: FC = () => {
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
if (currentTabName) {
|
|
if (currentTabName) {
|
|
- setSortBy(OwnedNftOrderByInput.CreatedAtDesc)
|
|
|
|
setCurrentTab(currentTabName)
|
|
setCurrentTab(currentTabName)
|
|
- setIsFiltersOpen(false)
|
|
|
|
}
|
|
}
|
|
- }, [currentTabName, setIsFiltersOpen])
|
|
|
|
|
|
+ }, [currentTabName])
|
|
|
|
|
|
if (!loadingMember && !member) {
|
|
if (!loadingMember && !member) {
|
|
return (
|
|
return (
|
|
@@ -167,51 +249,43 @@ export const MemberView: FC = () => {
|
|
loading={loadingMember}
|
|
loading={loadingMember}
|
|
isOwner={memberId === member?.id}
|
|
isOwner={memberId === member?.id}
|
|
/>
|
|
/>
|
|
- <TabsWrapper isFiltersOpen={isFiltersOpen}>
|
|
|
|
- <TabsContainer isMemberActivityTab={currentTab === 'Activity'}>
|
|
|
|
- <StyledTabs
|
|
|
|
- selected={TABS.findIndex((x) => x === currentTab)}
|
|
|
|
- initialIndex={0}
|
|
|
|
- tabs={mappedTabs}
|
|
|
|
- onSelectTab={handleSetCurrentTab}
|
|
|
|
- />
|
|
|
|
- {currentTab && ['NFTs owned', 'Activity'].includes(currentTab) && (
|
|
|
|
- <SortContainer>
|
|
|
|
- {currentTab === 'NFTs owned' ? (
|
|
|
|
- <Select
|
|
|
|
- size="medium"
|
|
|
|
- inlineLabel="Sort by"
|
|
|
|
- value={sortBy}
|
|
|
|
- items={NFT_SORT_OPTIONS}
|
|
|
|
- onChange={handleSorting}
|
|
|
|
- />
|
|
|
|
- ) : (
|
|
|
|
- <Select
|
|
|
|
- size="medium"
|
|
|
|
- inlineLabel="Sort by"
|
|
|
|
- value={sortByTimestamp}
|
|
|
|
- items={NFT_SORT_ACTIVITY_OPTIONS}
|
|
|
|
- onChange={handleSortingActivity}
|
|
|
|
- />
|
|
|
|
- )}
|
|
|
|
- </SortContainer>
|
|
|
|
- )}
|
|
|
|
- {currentTab === 'NFTs owned' && (
|
|
|
|
- <FilterButtonContainer>
|
|
|
|
- <Button
|
|
|
|
- badge={canClearAllFilters}
|
|
|
|
- variant="secondary"
|
|
|
|
- icon={<SvgActionFilters />}
|
|
|
|
- onClick={toggleFilters}
|
|
|
|
- >
|
|
|
|
- Filters
|
|
|
|
- </Button>
|
|
|
|
- </FilterButtonContainer>
|
|
|
|
- )}
|
|
|
|
- </TabsContainer>
|
|
|
|
- <FiltersBar {...filtersBarLogic} activeFilters={['nftStatus']} />
|
|
|
|
- </TabsWrapper>
|
|
|
|
- {tabContent}
|
|
|
|
|
|
+ <Section
|
|
|
|
+ headerProps={{
|
|
|
|
+ start: {
|
|
|
|
+ type: 'tabs',
|
|
|
|
+ tabsProps: {
|
|
|
|
+ selected: TABS.findIndex((t) => t === currentTabName),
|
|
|
|
+ tabs: mappedTabs,
|
|
|
|
+ onSelectTab: handleSetCurrentTab,
|
|
|
|
+ },
|
|
|
|
+ },
|
|
|
|
+ ...headerFilters,
|
|
|
|
+ }}
|
|
|
|
+ contentProps={{
|
|
|
|
+ type: 'grid',
|
|
|
|
+ grid: gridColumns,
|
|
|
|
+ children: tabContent,
|
|
|
|
+ }}
|
|
|
|
+ footerProps={
|
|
|
|
+ currentTab === 'NFTs'
|
|
|
|
+ ? {
|
|
|
|
+ type: 'infinite',
|
|
|
|
+ loadingTriggerOffset: InfiniteLoadingOffsets.NftTile,
|
|
|
|
+ reachedEnd: !pageInfo?.hasNextPage ?? true,
|
|
|
|
+ fetchMore: async () => {
|
|
|
|
+ if (pageInfo?.hasNextPage) {
|
|
|
|
+ await fetchMore({
|
|
|
|
+ variables: {
|
|
|
|
+ first: columns * 4,
|
|
|
|
+ after: pageInfo?.endCursor,
|
|
|
|
+ },
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ }
|
|
|
|
+ : undefined
|
|
|
|
+ }
|
|
|
|
+ />
|
|
</LimitedWidthContainer>
|
|
</LimitedWidthContainer>
|
|
</ViewWrapper>
|
|
</ViewWrapper>
|
|
)
|
|
)
|