feat: infinite scroll for hosted NFAs list

This commit is contained in:
Camila Sosa Morales 2023-05-11 11:38:03 -03:00
parent 698238c9b9
commit ca185c0f36
11 changed files with 204 additions and 83 deletions

View File

@ -22,50 +22,56 @@ query lastNFAsPaginated(
accessPoints { accessPoints {
id id
} }
} verified
}
query totalTokens {
tokens {
id
}
}
query getLatestNFAs {
tokens {
id
name
} }
} }
query getNFADetail($id: ID!) { query getNFADetail($id: ID!) {
token(id: $id) { token(id: $id) {
tokenId accessPoints {
owner {
id id
} }
name
description description
ENS
externalURL
logo
color color
createdAt createdAt
accessPoints { ENS
createdAt externalURL
contentVerified gitRepository {
owner { id
id }
} logo
name
owner {
id id
} }
verified verified
verifier { verifier {
id 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
} }
id
} }
} }

View File

@ -61,8 +61,7 @@ export const NFACard: React.FC<NFACardProps> = forwardStyledRef<
}} }}
> >
<S.Title title={data.name}>{data.name}</S.Title> <S.Title title={data.name}>{data.name}</S.Title>
{/* TODO: set correct value when it gets available on contract side */} <Badge verified={data.verified} />
<Badge verified={Math.random() > 0.5} />
</Flex> </Flex>
<Flex css={{ gap: '$1' }}> <Flex css={{ gap: '$1' }}>

View File

@ -46,6 +46,10 @@ const client = new ApolloClient({
keyArgs: ['where', 'orderBy', 'orderDirection'], keyArgs: ['where', 'orderBy', 'orderDirection'],
merge: mergeByKey('id'), merge: mergeByKey('id'),
}, },
accessPoints: {
keyArgs: ['where', 'orderBy', 'orderDirection'],
merge: mergeByKey('id'),
},
}, },
}, },
}, },

View File

@ -13,7 +13,9 @@ export const contractAddress = (address: string): string => {
export const getRepositoryFromURL = (url: string): string => { export const getRepositoryFromURL = (url: string): string => {
const urlSplitted = url.split('/'); const urlSplitted = url.split('/');
return `${urlSplitted[3]}/${urlSplitted[4]}`; return urlSplitted[3] && urlSplitted[4]
? `${urlSplitted[3]}/${urlSplitted[4]}`
: '';
}; };
export const getDate = (date: number): string => { export const getDate = (date: number): string => {

View File

@ -46,7 +46,9 @@ export const NFAListFragment: React.FC = () => {
skip: pageNumber * pageSize, //skip is for the pagination skip: pageNumber * pageSize, //skip is for the pagination
}, },
onCompleted: (data) => { onCompleted: (data) => {
if (data.tokens.length - tokens.length < pageSize) setEndReached(true); if (data.tokens.length - tokens.length < pageSize) {
setEndReached(true);
}
}, },
}); });

View File

@ -10,7 +10,7 @@ export type ExploreContext = {
pageNumber: number; pageNumber: number;
endReached: boolean; endReached: boolean;
setSearch: (search: string) => void; setSearch: (search: string) => void;
setOrderBy: (orderBy: string) => void; setOrderBy: (orderBy: Token_orderBy) => void;
setOrderDirection: (orderDirection: OrderDirection) => void; setOrderDirection: (orderDirection: OrderDirection) => void;
setPageNumber: (pageNumber: number) => void; setPageNumber: (pageNumber: number) => void;
setEndReached: (isEndReaced: boolean) => void; setEndReached: (isEndReaced: boolean) => void;
@ -29,7 +29,7 @@ export abstract class Explore {
children, children,
}: Explore.ProviderProps) => { }: Explore.ProviderProps) => {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [orderBy, setOrderBy] = useState('tokenId'); const [orderBy, setOrderBy] = useState<Token_orderBy>('tokenId');
const [orderDirection, setOrderDirection] = const [orderDirection, setOrderDirection] =
useState<OrderDirection>('desc'); useState<OrderDirection>('desc');
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);

View File

@ -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 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 { 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 { IndexedNFA } from '../../indexed-nfa.context';
import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; import { IndexedNFAStyles as S } from '../../indexed-nfa.styles';
import { SkeletonAccessPointsListFragment } from './skeleton.ap-list';
//TODO remove type AccessPointProps = {
const thumbnailMocks = [Rectangle1, Rectangle2, Rectangle3]; 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 = () => { export const AccessPointsListFragment: React.FC = () => {
const { const {
nfa: { accessPoints }, nfa: { tokenId },
orderDirection,
pageNumber,
endReached,
setEndReached,
setPageNumber,
} = IndexedNFA.useContext(); } = 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 ( return (
<S.Main.AccessPoint.List> <S.Main.AccessPoint.List>
{accessPoints && accessPoints?.length > 0 ? ( {accessPoints.map((item, index) => (
accessPoints.map((item, index) => ( <AccessPoint key={index} data={item} />
<S.Main.AccessPoint.Grid key={index}> ))}
<S.Main.AccessPoint.Thumbnail> {isLoading && <SkeletonAccessPointsListFragment />}
<img {!isLoading && accessPoints.length === 0 && (
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>
))
) : (
<S.Main.AccessPoint.NoResults> <S.Main.AccessPoint.NoResults>
<h2>No hosted NFAs</h2> <h2>No hosted NFAs</h2>
</S.Main.AccessPoint.NoResults> </S.Main.AccessPoint.NoResults>

View File

@ -1,28 +1,38 @@
import { useState } from 'react'; import { useState } from 'react';
import { OrderDirection } from '@/../.graphclient';
import { Combobox, Flex } from '@/components'; import { Combobox, Flex } from '@/components';
import { AppLog } from '@/utils';
import { IndexedNFA } from '../../indexed-nfa.context';
import { IndexedNFAStyles as S } from '../../indexed-nfa.styles'; import { IndexedNFAStyles as S } from '../../indexed-nfa.styles';
type SortItem = { type SortItem = {
value: string; value: OrderDirection;
label: string; label: string;
}; };
const orderResults: SortItem[] = [ const orderResults: SortItem[] = [
{ value: 'newest', label: 'Newest' }, { value: 'desc', label: 'Newest' },
{ value: 'oldest', label: 'Oldest' }, { value: 'asc', label: 'Oldest' },
]; ];
export const Header: React.FC = () => { export const Header: React.FC = () => {
const { setPageNumber, setOrderDirection, setEndReached } =
IndexedNFA.useContext();
const [selectedValue, setSelectedValue] = useState<SortItem>(orderResults[0]); const [selectedValue, setSelectedValue] = useState<SortItem>(orderResults[0]);
const handleSortChange = (item: SortItem | undefined): void => { const handleSortChange = (item: SortItem | undefined): void => {
//TODO integrate with context and sort
if (item) { if (item) {
setSelectedValue(item); setSelectedValue(item);
setPageNumber(0);
setEndReached(false);
setOrderDirection(item.value);
} else {
AppLog.errorToast('Error selecting sort option. Try again');
} }
}; };
return ( return (
<> <>
<Flex css={{ justifyContent: 'space-between', alignItems: 'center' }}> <Flex css={{ justifyContent: 'space-between', alignItems: 'center' }}>

View File

@ -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>
);

View File

@ -1,4 +1,5 @@
import { IndexedNFAStyles as S } from '../indexed-nfa.styles'; import { IndexedNFAStyles as S } from '../indexed-nfa.styles';
import { SkeletonAccessPointsListFragment } from './main/skeleton.ap-list';
export const IndexedNFASkeletonFragment: React.FC = () => ( export const IndexedNFASkeletonFragment: React.FC = () => (
<S.Grid> <S.Grid>
@ -7,10 +8,7 @@ export const IndexedNFASkeletonFragment: React.FC = () => (
</S.Aside.Container> </S.Aside.Container>
<S.Main.Container css={{ justifyContent: 'stretch' }}> <S.Main.Container css={{ justifyContent: 'stretch' }}>
<S.Skeleton css={{ height: '2.875rem' }} /> <S.Skeleton css={{ height: '2.875rem' }} />
<S.Skeleton css={{ height: '1.5rem' }} /> <SkeletonAccessPointsListFragment />
<S.Main.Divider.Line />
<S.Skeleton css={{ height: '10rem' }} />
<S.Skeleton css={{ height: '15rem' }} />
</S.Main.Container> </S.Main.Container>
</S.Grid> </S.Grid>
); );

View File

@ -1,4 +1,6 @@
import { Owner, Token } from '@/graphclient'; import { useState } from 'react';
import { OrderDirection, Owner, Token } from '@/graphclient';
import { createContext } from '@/utils'; import { createContext } from '@/utils';
const [Provider, useContext] = createContext<IndexedNFA.Context>({ const [Provider, useContext] = createContext<IndexedNFA.Context>({
@ -10,7 +12,21 @@ const [Provider, useContext] = createContext<IndexedNFA.Context>({
export const IndexedNFA = { export const IndexedNFA = {
useContext, useContext,
Provider: ({ children, nfa }: IndexedNFA.ProviderProps): JSX.Element => { 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'> & { nfa: Omit<Token, 'mintTransaction' | 'id' | 'owner'> & {
owner: Pick<Owner, 'id'>; 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 = { export type ProviderProps = {