diff --git a/ui/src/components/layout/index.ts b/ui/src/components/layout/index.ts
index 8e3e9fa..a0c4cdf 100644
--- a/ui/src/components/layout/index.ts
+++ b/ui/src/components/layout/index.ts
@@ -3,3 +3,4 @@ export * from './flex.styles';
export * from './stepper';
export * from './nav-bar';
export * from './page';
+export * from './skeleton.styles';
diff --git a/ui/src/components/layout/skeleton.styles.ts b/ui/src/components/layout/skeleton.styles.ts
new file mode 100644
index 0000000..b54f820
--- /dev/null
+++ b/ui/src/components/layout/skeleton.styles.ts
@@ -0,0 +1,20 @@
+import { dripStitches } from '@/theme';
+
+const { styled, keyframes } = dripStitches;
+
+const SkeletonKeyframes = keyframes({
+ '0%': {
+ opacity: '1',
+ },
+ '50%': {
+ opacity: '0.5',
+ },
+ '100%': {
+ opacity: '1',
+ },
+});
+
+export const Skeleton = styled('div', {
+ animation: `${SkeletonKeyframes} 1s ease-in-out infinite`,
+ backgroundColor: '$slate6',
+});
diff --git a/ui/src/components/nfa-card/index.ts b/ui/src/components/nfa-card/index.ts
index f8fa163..a2f2569 100644
--- a/ui/src/components/nfa-card/index.ts
+++ b/ui/src/components/nfa-card/index.ts
@@ -1 +1,2 @@
export * from './nfa-card';
+export * from './nfa-card-skeleton';
diff --git a/ui/src/components/nfa-card/nfa-card-skeleton.tsx b/ui/src/components/nfa-card/nfa-card-skeleton.tsx
new file mode 100644
index 0000000..86d509a
--- /dev/null
+++ b/ui/src/components/nfa-card/nfa-card-skeleton.tsx
@@ -0,0 +1,13 @@
+import { NFACardStyles as S } from './nfa-card.styles';
+
+export const NFACardSkeleton: React.FC = () => {
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/ui/src/components/nfa-card/nfa-card.styles.ts b/ui/src/components/nfa-card/nfa-card.styles.ts
index 23e1f57..9e3ca44 100644
--- a/ui/src/components/nfa-card/nfa-card.styles.ts
+++ b/ui/src/components/nfa-card/nfa-card.styles.ts
@@ -2,6 +2,8 @@ import { Link } from 'react-router-dom';
import { dripStitches } from '@/theme';
+import { Skeleton } from '../layout';
+
const { styled } = dripStitches;
export const NFACardStyles = {
@@ -75,4 +77,23 @@ export const NFACardStyles = {
},
},
}),
+
+ Skeleton: {
+ Preview: styled(Skeleton, {
+ width: '100%',
+ height: 'calc(14.6875rem - 2px)',
+ }),
+
+ Title: styled(Skeleton, {
+ width: '100%',
+ height: 'calc(1.4 * $fontSizes$xl)',
+ borderRadius: '$lg',
+ }),
+
+ Content: styled(Skeleton, {
+ width: '100%',
+ height: 'calc(1.43 * $fontSizes$sm)',
+ borderRadius: '$lg',
+ }),
+ },
};
diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts
index 4d095a7..c32fad0 100644
--- a/ui/src/hooks/index.ts
+++ b/ui/src/hooks/index.ts
@@ -1 +1,3 @@
export * from './use-transaction-cost';
+export * from './use-window-scroll-end';
+export * from './use-debounce';
diff --git a/ui/src/hooks/use-window-scroll-end.ts b/ui/src/hooks/use-window-scroll-end.ts
new file mode 100644
index 0000000..3b43647
--- /dev/null
+++ b/ui/src/hooks/use-window-scroll-end.ts
@@ -0,0 +1,20 @@
+import { useEffect } from 'react';
+
+import { useDebounce } from './use-debounce';
+
+export const useWindowScrollEnd = (
+ callback: () => void,
+ threshold = 0.3 // threshold used to recognize scroll end (30% of the remaining scroll)
+): void => {
+ const debounced = useDebounce(() => {
+ const { scrollHeight, scrollTop, offsetHeight } = document.documentElement;
+ if (scrollHeight * (1 - threshold) > scrollTop + offsetHeight) return;
+ callback();
+ }, 100);
+
+ useEffect(() => {
+ debounced();
+ window.addEventListener('scroll', debounced);
+ return () => window.removeEventListener('scroll', debounced);
+ }, [debounced]);
+};
diff --git a/ui/src/providers/apollo-provider.tsx b/ui/src/providers/apollo-provider.tsx
index bdf799c..1ab36f7 100644
--- a/ui/src/providers/apollo-provider.tsx
+++ b/ui/src/providers/apollo-provider.tsx
@@ -1,6 +1,7 @@
import {
ApolloClient,
ApolloProvider as Provider,
+ FieldMergeFunction,
InMemoryCache,
} from '@apollo/client';
import { GraphApolloLink } from '@graphprotocol/client-apollo';
@@ -8,9 +9,47 @@ import React from 'react';
import * as GraphClient from '@/graphclient';
+// https://www.apollographql.com/docs/react/caching/cache-field-behavior/#merging-arrays-of-non-normalized-objects
+/* eslint-disable @typescript-eslint/no-explicit-any */
+const mergeByKey =
+ (key: string): FieldMergeFunction =>
+ (existing: any[] = [], incoming: any[], { mergeObjects, readField }) => {
+ const merged: any[] = existing ? existing.slice(0) : [];
+ const keyToIndex: Record = Object.create(null);
+ if (existing) {
+ existing.forEach((token, index) => {
+ const id = readField(key, token) as number;
+ keyToIndex[id] = index;
+ });
+ }
+ incoming.forEach((token) => {
+ const id = readField(key, token) as number;
+ const index = keyToIndex[id];
+ if (typeof index === 'number') {
+ merged[index] = mergeObjects(merged[index], token);
+ } else {
+ keyToIndex[id] = merged.length;
+ merged.push(token);
+ }
+ });
+ return merged;
+ };
+/** eslint-enable @typescript-eslint/no-explicit-any */
+
const client = new ApolloClient({
link: new GraphApolloLink(GraphClient),
- cache: new InMemoryCache(),
+ cache: new InMemoryCache({
+ typePolicies: {
+ Query: {
+ fields: {
+ tokens: {
+ keyArgs: ['where'],
+ merge: mergeByKey('id'),
+ },
+ },
+ },
+ },
+ }),
});
type ApolloProviderProps = {
diff --git a/ui/src/views/explore/list-nfas/nfa-list/nfa-list.tsx b/ui/src/views/explore/list-nfas/nfa-list/nfa-list.tsx
index 5aa4beb..6dbc468 100644
--- a/ui/src/views/explore/list-nfas/nfa-list/nfa-list.tsx
+++ b/ui/src/views/explore/list-nfas/nfa-list/nfa-list.tsx
@@ -1,78 +1,69 @@
-/* eslint-disable react/react-in-jsx-scope */
import { useQuery } from '@apollo/client';
import { useEffect, useState } from 'react';
-import { Button, Flex, NFACard, NoResults } from '@/components';
-import { lastNFAsPaginatedDocument, totalTokensDocument } from '@/graphclient';
+import { Flex, NFACard, NFACardSkeleton, NoResults } from '@/components';
+import { lastNFAsPaginatedDocument } from '@/graphclient';
+import { useWindowScrollEnd } from '@/hooks';
const pageSize = 10; //Set this size to test pagination
+const LoadingSkeletons: React.FC = () => (
+ <>
+
+
+
+
+ >
+);
+
export const NFAList: React.FC = () => {
- const [pageNumber, setPageNumber] = useState(1);
- const [totalPages, setTotalPages] = useState(0);
+ const [pageNumber, setPageNumber] = useState(0);
+ const [endReached, setEndReached] = useState(false);
const {
- data: totalTokens,
- loading: loadingTotalTokens,
- error: errorTotalTokens,
- } = useQuery(totalTokensDocument);
-
- const {
- data: dataMintedTokens,
- loading: loadingMintedTokens,
- error: errorMintedTokens,
+ data: { tokens } = { tokens: [] },
+ loading: isLoading,
+ error: queryError,
} = useQuery(lastNFAsPaginatedDocument, {
+ fetchPolicy: 'cache-and-network',
variables: {
- //first page is 0
pageSize,
- skip: pageNumber > 0 ? (pageNumber - 1) * pageSize : pageNumber,
+ skip: pageNumber * pageSize,
+ },
+ onCompleted: (data) => {
+ if (data.tokens.length - tokens.length < pageSize) setEndReached(true);
},
});
useEffect(() => {
- if (totalTokens && totalTokens.tokens.length > 0) {
- setTotalPages(Math.ceil(totalTokens.tokens.length / pageSize));
- }
- }, [totalTokens]);
+ // Update page number when there are cached tokens
+ setPageNumber(Math.ceil(tokens.length / pageSize));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
- if (loadingMintedTokens || loadingTotalTokens) return Loading...
; //TODO handle loading
- if (errorMintedTokens || errorTotalTokens) return Error
; //TODO handle error
+ useWindowScrollEnd(() => {
+ if (isLoading || endReached) return;
+ setPageNumber((prevState) => prevState + 1);
+ });
- const handlePreviousPage = (): void => {
- if (pageNumber > 1) {
- setPageNumber((prevState) => prevState - 1);
- }
- };
-
- const handleNextPage = (): void => {
- if (pageNumber + 1 <= totalPages)
- setPageNumber((prevState) => prevState + 1);
- };
+ if (queryError) return Error
; //TODO handle error
return (
-
-
- {/* TODO this will be remove when we have pagination component */}
- items per page: {pageSize}
-
- page: {pageNumber}/{totalPages}
-
-
-
-
-
+
- {dataMintedTokens && dataMintedTokens.tokens.length > 0 ? (
- dataMintedTokens.tokens.map((mint) => (
-
- ))
- ) : (
-
- )}
+ {tokens.map((token) => (
+
+ ))}
+ {isLoading && }
+ {!isLoading && tokens.length === 0 && }
);