feat: infinite scroll for hosted NFAs list
This commit is contained in:
parent
698238c9b9
commit
ca185c0f36
|
|
@ -22,50 +22,56 @@ query lastNFAsPaginated(
|
|||
accessPoints {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query totalTokens {
|
||||
tokens {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
query getLatestNFAs {
|
||||
tokens {
|
||||
id
|
||||
name
|
||||
verified
|
||||
}
|
||||
}
|
||||
|
||||
query getNFADetail($id: ID!) {
|
||||
token(id: $id) {
|
||||
tokenId
|
||||
owner {
|
||||
accessPoints {
|
||||
id
|
||||
}
|
||||
name
|
||||
description
|
||||
ENS
|
||||
externalURL
|
||||
logo
|
||||
color
|
||||
createdAt
|
||||
accessPoints {
|
||||
createdAt
|
||||
contentVerified
|
||||
owner {
|
||||
id
|
||||
}
|
||||
ENS
|
||||
externalURL
|
||||
gitRepository {
|
||||
id
|
||||
}
|
||||
logo
|
||||
name
|
||||
owner {
|
||||
id
|
||||
}
|
||||
verified
|
||||
verifier {
|
||||
id
|
||||
}
|
||||
gitRepository {
|
||||
tokenId
|
||||
}
|
||||
}
|
||||
|
||||
query getAccessPointsNFA(
|
||||
$tokenId: String!
|
||||
$orderBy: AccessPoint_orderBy
|
||||
$orderDirection: OrderDirection
|
||||
$pageSize: Int
|
||||
$skip: Int
|
||||
) {
|
||||
accessPoints(
|
||||
where: { token: $tokenId }
|
||||
orderDirection: $orderDirection
|
||||
orderBy: $orderBy
|
||||
first: $pageSize
|
||||
skip: $skip
|
||||
) {
|
||||
contentVerified
|
||||
createdAt
|
||||
owner {
|
||||
id
|
||||
}
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,8 +61,7 @@ export const NFACard: React.FC<NFACardProps> = forwardStyledRef<
|
|||
}}
|
||||
>
|
||||
<S.Title title={data.name}>{data.name}</S.Title>
|
||||
{/* TODO: set correct value when it gets available on contract side */}
|
||||
<Badge verified={Math.random() > 0.5} />
|
||||
<Badge verified={data.verified} />
|
||||
</Flex>
|
||||
|
||||
<Flex css={{ gap: '$1' }}>
|
||||
|
|
|
|||
|
|
@ -46,6 +46,10 @@ const client = new ApolloClient({
|
|||
keyArgs: ['where', 'orderBy', 'orderDirection'],
|
||||
merge: mergeByKey('id'),
|
||||
},
|
||||
accessPoints: {
|
||||
keyArgs: ['where', 'orderBy', 'orderDirection'],
|
||||
merge: mergeByKey('id'),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export const contractAddress = (address: string): string => {
|
|||
|
||||
export const getRepositoryFromURL = (url: string): string => {
|
||||
const urlSplitted = url.split('/');
|
||||
return `${urlSplitted[3]}/${urlSplitted[4]}`;
|
||||
return urlSplitted[3] && urlSplitted[4]
|
||||
? `${urlSplitted[3]}/${urlSplitted[4]}`
|
||||
: '';
|
||||
};
|
||||
|
||||
export const getDate = (date: number): string => {
|
||||
|
|
|
|||
|
|
@ -46,7 +46,9 @@ export const NFAListFragment: React.FC = () => {
|
|||
skip: pageNumber * pageSize, //skip is for the pagination
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
if (data.tokens.length - tokens.length < pageSize) setEndReached(true);
|
||||
if (data.tokens.length - tokens.length < pageSize) {
|
||||
setEndReached(true);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export type ExploreContext = {
|
|||
pageNumber: number;
|
||||
endReached: boolean;
|
||||
setSearch: (search: string) => void;
|
||||
setOrderBy: (orderBy: string) => void;
|
||||
setOrderBy: (orderBy: Token_orderBy) => void;
|
||||
setOrderDirection: (orderDirection: OrderDirection) => void;
|
||||
setPageNumber: (pageNumber: number) => void;
|
||||
setEndReached: (isEndReaced: boolean) => void;
|
||||
|
|
@ -29,7 +29,7 @@ export abstract class Explore {
|
|||
children,
|
||||
}: Explore.ProviderProps) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [orderBy, setOrderBy] = useState('tokenId');
|
||||
const [orderBy, setOrderBy] = useState<Token_orderBy>('tokenId');
|
||||
const [orderDirection, setOrderDirection] =
|
||||
useState<OrderDirection>('desc');
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
|
|
|
|||
|
|
@ -1,57 +1,114 @@
|
|||
import { useQuery } from '@apollo/client';
|
||||
import { ethers } from 'ethers';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import Rectangle1 from '@/assets/Rectangle-199.png';
|
||||
import Rectangle2 from '@/assets/Rectangle-200.png';
|
||||
import Rectangle3 from '@/assets/Rectangle-201.png';
|
||||
import { Flex, ResolvedAddress, Text } from '@/components';
|
||||
import { getTimeSince } from '@/utils';
|
||||
import {
|
||||
AccessPoint as AccessPointType,
|
||||
getAccessPointsNFADocument,
|
||||
Owner,
|
||||
} from '@/graphclient';
|
||||
import { useWindowScrollEnd } from '@/hooks';
|
||||
import { AppLog, getTimeSince } from '@/utils';
|
||||
|
||||
import { IndexedNFA } from '../../indexed-nfa.context';
|
||||
import { IndexedNFAStyles as S } from '../../indexed-nfa.styles';
|
||||
import { SkeletonAccessPointsListFragment } from './skeleton.ap-list';
|
||||
|
||||
//TODO remove
|
||||
const thumbnailMocks = [Rectangle1, Rectangle2, Rectangle3];
|
||||
type AccessPointProps = {
|
||||
data: Pick<AccessPointType, 'id' | 'contentVerified' | 'createdAt'> & {
|
||||
owner: Pick<Owner, 'id'>;
|
||||
};
|
||||
};
|
||||
|
||||
const AccessPoint: React.FC<AccessPointProps> = ({
|
||||
data,
|
||||
}: AccessPointProps) => {
|
||||
const { id: name, owner, createdAt } = data;
|
||||
return (
|
||||
<S.Main.AccessPoint.Grid>
|
||||
<S.Main.AccessPoint.Thumbnail>
|
||||
<img src={Rectangle1} />
|
||||
</S.Main.AccessPoint.Thumbnail>
|
||||
<S.Main.AccessPoint.Data.Container>
|
||||
<S.Main.AccessPoint.Title>{name}</S.Main.AccessPoint.Title>
|
||||
<Flex css={{ gap: '$2h', alignItems: 'center', textAlign: 'center' }}>
|
||||
<Text css={{ color: '$slate11' }}>
|
||||
<ResolvedAddress>{owner.id}</ResolvedAddress>
|
||||
</Text>
|
||||
<S.Main.Divider.Elipse />
|
||||
{/* TODO get from bunny CDN */}
|
||||
<Text css={{ color: '$slate11' }}>220 views</Text>
|
||||
<S.Main.Divider.Elipse />
|
||||
<Text css={{ color: '$slate11' }}>{getTimeSince(createdAt)}</Text>
|
||||
</Flex>
|
||||
</S.Main.AccessPoint.Data.Container>
|
||||
</S.Main.AccessPoint.Grid>
|
||||
);
|
||||
};
|
||||
|
||||
const pageSize = 10; //Set this size to test pagination
|
||||
|
||||
export const AccessPointsListFragment: React.FC = () => {
|
||||
const {
|
||||
nfa: { accessPoints },
|
||||
nfa: { tokenId },
|
||||
orderDirection,
|
||||
pageNumber,
|
||||
endReached,
|
||||
setEndReached,
|
||||
setPageNumber,
|
||||
} = IndexedNFA.useContext();
|
||||
|
||||
const handleError = (error: unknown): void => {
|
||||
AppLog.errorToast(
|
||||
'There was an error trying to get the access points',
|
||||
error
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
loading: isLoading,
|
||||
data: { accessPoints } = { accessPoints: [] },
|
||||
error: queryError,
|
||||
} = useQuery(getAccessPointsNFADocument, {
|
||||
skip: tokenId === undefined,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
variables: {
|
||||
tokenId: ethers.utils.hexlify(Number(tokenId)),
|
||||
orderDirection: orderDirection,
|
||||
orderBy: 'createdAt',
|
||||
pageSize,
|
||||
skip: pageNumber * pageSize, //skip is for the pagination
|
||||
},
|
||||
onCompleted(data) {
|
||||
if (data.accessPoints.length - accessPoints.length < pageSize)
|
||||
setEndReached(true);
|
||||
},
|
||||
onError(error) {
|
||||
handleError(error);
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Update page number when there are cached tokens
|
||||
setPageNumber(Math.ceil(accessPoints.length / pageSize));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useWindowScrollEnd(() => {
|
||||
// debugger;
|
||||
if (isLoading || endReached || queryError) return;
|
||||
setPageNumber(pageNumber + 1);
|
||||
});
|
||||
|
||||
return (
|
||||
<S.Main.AccessPoint.List>
|
||||
{accessPoints && accessPoints?.length > 0 ? (
|
||||
accessPoints.map((item, index) => (
|
||||
<S.Main.AccessPoint.Grid key={index}>
|
||||
<S.Main.AccessPoint.Thumbnail>
|
||||
<img
|
||||
src={
|
||||
thumbnailMocks[
|
||||
Math.floor(
|
||||
Math.random() * (Math.floor(2) - Math.ceil(0) + 1) +
|
||||
Math.ceil(0)
|
||||
)
|
||||
]
|
||||
}
|
||||
/>
|
||||
</S.Main.AccessPoint.Thumbnail>
|
||||
<S.Main.AccessPoint.Data.Container>
|
||||
<S.Main.AccessPoint.Title>{item.id}</S.Main.AccessPoint.Title>
|
||||
<Flex
|
||||
css={{ gap: '$2h', alignItems: 'center', textAlign: 'center' }}
|
||||
>
|
||||
<Text css={{ color: '$slate11' }}>
|
||||
<ResolvedAddress>{item.owner.id}</ResolvedAddress>
|
||||
</Text>
|
||||
<S.Main.Divider.Elipse />
|
||||
{/* TODO get from bunny CDN */}
|
||||
<Text css={{ color: '$slate11' }}>220 views</Text>
|
||||
<S.Main.Divider.Elipse />
|
||||
<Text css={{ color: '$slate11' }}>
|
||||
{getTimeSince(item.createdAt)}
|
||||
</Text>
|
||||
</Flex>
|
||||
</S.Main.AccessPoint.Data.Container>
|
||||
</S.Main.AccessPoint.Grid>
|
||||
))
|
||||
) : (
|
||||
{accessPoints.map((item, index) => (
|
||||
<AccessPoint key={index} data={item} />
|
||||
))}
|
||||
{isLoading && <SkeletonAccessPointsListFragment />}
|
||||
{!isLoading && accessPoints.length === 0 && (
|
||||
<S.Main.AccessPoint.NoResults>
|
||||
<h2>No hosted NFAs</h2>
|
||||
</S.Main.AccessPoint.NoResults>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,38 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { OrderDirection } from '@/../.graphclient';
|
||||
import { Combobox, Flex } from '@/components';
|
||||
import { AppLog } from '@/utils';
|
||||
|
||||
import { IndexedNFA } from '../../indexed-nfa.context';
|
||||
import { IndexedNFAStyles as S } from '../../indexed-nfa.styles';
|
||||
|
||||
type SortItem = {
|
||||
value: string;
|
||||
value: OrderDirection;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const orderResults: SortItem[] = [
|
||||
{ value: 'newest', label: 'Newest' },
|
||||
{ value: 'oldest', label: 'Oldest' },
|
||||
{ value: 'desc', label: 'Newest' },
|
||||
{ value: 'asc', label: 'Oldest' },
|
||||
];
|
||||
|
||||
export const Header: React.FC = () => {
|
||||
const { setPageNumber, setOrderDirection, setEndReached } =
|
||||
IndexedNFA.useContext();
|
||||
const [selectedValue, setSelectedValue] = useState<SortItem>(orderResults[0]);
|
||||
|
||||
const handleSortChange = (item: SortItem | undefined): void => {
|
||||
//TODO integrate with context and sort
|
||||
if (item) {
|
||||
setSelectedValue(item);
|
||||
setPageNumber(0);
|
||||
setEndReached(false);
|
||||
setOrderDirection(item.value);
|
||||
} else {
|
||||
AppLog.errorToast('Error selecting sort option. Try again');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex css={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
import { IndexedNFAStyles as S } from '../../indexed-nfa.styles';
|
||||
|
||||
const SkeletonAccessPoint: React.FC = () => (
|
||||
<S.Main.AccessPoint.Grid>
|
||||
<S.Main.AccessPoint.Thumbnail>
|
||||
<S.Skeleton css={{ height: '6rem' }} />
|
||||
</S.Main.AccessPoint.Thumbnail>
|
||||
<S.Main.AccessPoint.Data.Container>
|
||||
<S.Skeleton css={{ height: '2rem' }} />
|
||||
<S.Skeleton css={{ height: '1.25rem' }} />
|
||||
</S.Main.AccessPoint.Data.Container>
|
||||
</S.Main.AccessPoint.Grid>
|
||||
);
|
||||
|
||||
export const SkeletonAccessPointsListFragment: React.FC = () => (
|
||||
<S.Main.AccessPoint.List>
|
||||
<SkeletonAccessPoint />
|
||||
<SkeletonAccessPoint />
|
||||
<SkeletonAccessPoint />
|
||||
</S.Main.AccessPoint.List>
|
||||
);
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
|
||||
import { SkeletonAccessPointsListFragment } from './main/skeleton.ap-list';
|
||||
|
||||
export const IndexedNFASkeletonFragment: React.FC = () => (
|
||||
<S.Grid>
|
||||
|
|
@ -7,10 +8,7 @@ export const IndexedNFASkeletonFragment: React.FC = () => (
|
|||
</S.Aside.Container>
|
||||
<S.Main.Container css={{ justifyContent: 'stretch' }}>
|
||||
<S.Skeleton css={{ height: '2.875rem' }} />
|
||||
<S.Skeleton css={{ height: '1.5rem' }} />
|
||||
<S.Main.Divider.Line />
|
||||
<S.Skeleton css={{ height: '10rem' }} />
|
||||
<S.Skeleton css={{ height: '15rem' }} />
|
||||
<SkeletonAccessPointsListFragment />
|
||||
</S.Main.Container>
|
||||
</S.Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import { Owner, Token } from '@/graphclient';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { OrderDirection, Owner, Token } from '@/graphclient';
|
||||
import { createContext } from '@/utils';
|
||||
|
||||
const [Provider, useContext] = createContext<IndexedNFA.Context>({
|
||||
|
|
@ -10,7 +12,21 @@ const [Provider, useContext] = createContext<IndexedNFA.Context>({
|
|||
export const IndexedNFA = {
|
||||
useContext,
|
||||
Provider: ({ children, nfa }: IndexedNFA.ProviderProps): JSX.Element => {
|
||||
return <Provider value={{ nfa }}>{children}</Provider>;
|
||||
const [orderDirection, setOrderDirection] =
|
||||
useState<OrderDirection>('desc');
|
||||
const [pageNumber, setPageNumber] = useState(0);
|
||||
const [endReached, setEndReached] = useState(false);
|
||||
|
||||
const context = {
|
||||
nfa,
|
||||
orderDirection,
|
||||
pageNumber,
|
||||
endReached,
|
||||
setOrderDirection,
|
||||
setPageNumber,
|
||||
setEndReached,
|
||||
};
|
||||
return <Provider value={context}>{children}</Provider>;
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -19,6 +35,12 @@ export namespace IndexedNFA {
|
|||
nfa: Omit<Token, 'mintTransaction' | 'id' | 'owner'> & {
|
||||
owner: Pick<Owner, 'id'>;
|
||||
};
|
||||
orderDirection: OrderDirection;
|
||||
pageNumber: number;
|
||||
endReached: boolean;
|
||||
setOrderDirection: (orderDirection: OrderDirection) => void;
|
||||
setPageNumber: (pageNumber: number) => void;
|
||||
setEndReached: (isEndReaced: boolean) => void;
|
||||
};
|
||||
|
||||
export type ProviderProps = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue