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 {
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
}
}

View File

@ -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' }}>

View File

@ -46,6 +46,10 @@ const client = new ApolloClient({
keyArgs: ['where', 'orderBy', 'orderDirection'],
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 => {
const urlSplitted = url.split('/');
return `${urlSplitted[3]}/${urlSplitted[4]}`;
return urlSplitted[3] && urlSplitted[4]
? `${urlSplitted[3]}/${urlSplitted[4]}`
: '';
};
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
},
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;
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);

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 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>

View File

@ -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' }}>

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

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';
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 = {