feat: UI switch grid list on nfa listing (#261)

* feat: display nfas grid or list

* feat: add list view

* chore: rename component

* style: add margin

* chore: add skeleton for nfa list

* chore: add TODO comment

* Update ui/src/views/explore/explore-list/nfa-list/nfa-list.styles.ts

Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com>

* Update ui/src/views/explore/explore-list/nfa-list/nfa-list.styles.ts

Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com>

* merge develop

* style: responsiveness for explore view

* chore: remove old file

---------

Co-authored-by: Felipe Mendes <zo.fmendes@gmail.com>
This commit is contained in:
Camila Sosa Morales 2023-05-17 16:10:37 -03:00 committed by GitHub
parent 3a4cd5fa7d
commit 6cf32bedb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 368 additions and 90 deletions

View File

@ -22,6 +22,9 @@ query lastNFAsPaginated(
accessPoints { accessPoints {
id id
} }
owner {
id
}
verified verified
} }
} }

View File

@ -65,7 +65,6 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
return props[size as 'sm' | 'md' | 'lg']; return props[size as 'sm' | 'md' | 'lg'];
}, [size]); }, [size]);
return ( return (
<Button <Button
ref={ref} ref={ref}
@ -73,7 +72,6 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
size={size} size={size}
{...rest} {...rest}
css={{ css={{
padding: 0,
minWidth, minWidth,
fontSize, fontSize,
borderRadius: isRound ? '$full' : undefined, borderRadius: isRound ? '$full' : undefined,

View File

@ -1,6 +1,7 @@
import { AiFillCheckCircle } from '@react-icons/all-files/ai/AiFillCheckCircle'; import { AiFillCheckCircle } from '@react-icons/all-files/ai/AiFillCheckCircle';
import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck'; import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck';
import { AiOutlineTwitter } from '@react-icons/all-files/ai/AiOutlineTwitter'; import { AiOutlineTwitter } from '@react-icons/all-files/ai/AiOutlineTwitter';
import { AiOutlineUnorderedList } from '@react-icons/all-files/ai/AiOutlineUnorderedList';
import { BiGitBranch } from '@react-icons/all-files/bi/BiGitBranch'; import { BiGitBranch } from '@react-icons/all-files/bi/BiGitBranch';
import { BiSearch } from '@react-icons/all-files/bi/BiSearch'; import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill'; import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill';
@ -12,6 +13,7 @@ import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCi
import { IoCheckmarkCircleSharp } from '@react-icons/all-files/io5/IoCheckmarkCircleSharp'; import { IoCheckmarkCircleSharp } from '@react-icons/all-files/io5/IoCheckmarkCircleSharp';
import { IoClose } from '@react-icons/all-files/io5/IoClose'; import { IoClose } from '@react-icons/all-files/io5/IoClose';
import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp'; import { IoCloudUploadSharp } from '@react-icons/all-files/io5/IoCloudUploadSharp';
import { IoGridOutline } from '@react-icons/all-files/io5/IoGridOutline';
import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp'; import { IoInformationCircleSharp } from '@react-icons/all-files/io5/IoInformationCircleSharp';
import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub'; import { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
import { MdVerifiedUser } from '@react-icons/all-files/md/MdVerifiedUser'; import { MdVerifiedUser } from '@react-icons/all-files/md/MdVerifiedUser';
@ -43,7 +45,9 @@ export const IconLibrary = Object.freeze({
'fleek-logo': FleekLogo, 'fleek-logo': FleekLogo,
'fleek-name': FleekName, 'fleek-name': FleekName,
github: IoLogoGithub, github: IoLogoGithub,
grid: IoGridOutline,
info: IoInformationCircleSharp, info: IoInformationCircleSharp,
list: AiOutlineUnorderedList,
menu: FaBars, menu: FaBars,
metamask: MetamaskIcon, //remove if not used metamask: MetamaskIcon, //remove if not used
opensea: OpenseaIcon, opensea: OpenseaIcon,

View File

@ -1,12 +1,12 @@
import { Explore } from '../explore.context'; import { Explore } from '../explore.context';
import { NFAListFragment } from './nfa-list.fragment'; import { NFAsContainerFragment } from './nfa-list';
import { NFASearchFragment } from './nfa-search.fragment'; import { NFASearchFragment } from './nfa-search.fragment';
export const ExploreListFragment: React.FC = () => { export const ExploreListFragment: React.FC = () => {
return ( return (
<Explore.Provider> <Explore.Provider>
<NFASearchFragment /> <NFASearchFragment />
<NFAListFragment /> <NFAsContainerFragment />
</Explore.Provider> </Explore.Provider>
); );
}; };

View File

@ -1,24 +0,0 @@
import { styled } from '@/theme';
export const NFAListFragmentStyles = {
Container: styled('div', {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(12.5rem, 1fr))',
alignItems: 'flex-start',
flexWrap: 'wrap',
gap: '$6',
my: '$6',
minHeight: '50vh',
marginBottom: '30vh', // TODO: remove this if we add page footer
'@media (min-width: 1080px)': {
gridTemplateColumns: 'repeat(auto-fill, minmax(15rem, 1fr))',
},
}),
EmptyMessage: styled('span', {
padding: '$2 $3 $4 $3',
textAlign: 'center',
color: '$slate11',
width: '100%',
}),
};

View File

@ -0,0 +1 @@
export * from './nfas-container.fragment';

View File

@ -0,0 +1,37 @@
import { NFACard, NFACardSkeleton } from '@/components';
import { lastNFAsPaginatedQuery } from '@/graphclient';
import { NFAListFragmentStyles as S } from './nfa-list.styles';
const LoadingSkeletons: React.FC = () => (
<>
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
</>
);
type NFAGridFragmentProps = {
tokens: Array<lastNFAsPaginatedQuery['tokens'][0]>;
isLoading: boolean;
};
export const NFAGridFragment: React.FC<NFAGridFragmentProps> = ({
tokens,
isLoading,
}: NFAGridFragmentProps) => (
<S.Container>
{tokens.map((token) => (
<NFACard data={token} key={token.id} />
))}
{isLoading && <LoadingSkeletons />}
{!isLoading && tokens.length === 0 && (
<S.EmptyMessage>Nothing found.</S.EmptyMessage>
)}
</S.Container>
);

View File

@ -0,0 +1,53 @@
import { Text } from '@/components';
import { lastNFAsPaginatedQuery } from '@/graphclient';
import { NFAListFragmentStyles as S } from './nfa-list.styles';
import { NFARow } from './nfa-row.fragment';
import { NFARowSkeletonFragment } from './nfa-row.skeleton';
const LoadingListSkeleton: React.FC = () => (
<>
<NFARowSkeletonFragment />
<NFARowSkeletonFragment />
<NFARowSkeletonFragment />
</>
);
type NFAListFragmentProps = {
tokens: Array<lastNFAsPaginatedQuery['tokens'][0]>;
isLoading: boolean;
};
export const NFAListFragment: React.FC<NFAListFragmentProps> = ({
tokens,
isLoading,
}: NFAListFragmentProps) => {
return (
<S.Table.Container>
<S.Table.Root>
<S.Table.Head>
<S.Table.Row>
<S.Table.Data>NAME</S.Table.Data>
<S.Table.Data># HOSTED</S.Table.Data>
<S.Table.Data>Owner</S.Table.Data>
</S.Table.Row>
</S.Table.Head>
<S.Table.Body>
{tokens.map((token) => (
<NFARow token={token} key={token.id} />
))}
{isLoading && <LoadingListSkeleton />}
{!isLoading && tokens.length === 0 && (
<S.Table.Row>
<S.Table.Data align="center" colSpan={5}>
<Text>No results</Text>
</S.Table.Data>
</S.Table.Row>
)}
</S.Table.Body>
</S.Table.Root>
</S.Table.Container>
);
};

View File

@ -0,0 +1,71 @@
import { Skeleton } from '@/components';
import { styled } from '@/theme';
export const NFAListFragmentStyles = {
Container: styled('div', {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(12.5rem, 1fr))',
alignItems: 'flex-start',
flexWrap: 'wrap',
gap: '$6',
my: '$6',
minHeight: '50vh',
marginBottom: '30vh', // TODO: remove this if we add page footer
'@media (min-width: 1080px)': {
gridTemplateColumns: 'repeat(auto-fill, minmax(15rem, 1fr))',
},
}),
EmptyMessage: styled('span', {
padding: '$2 $3 $4 $3',
textAlign: 'center',
color: '$slate11',
width: '100%',
}),
Table: {
Container: styled('div', {
marginTop: '$6',
padding: '0 $5',
// maxHeight: '15.125rem',
overflow: 'auto',
}),
Root: styled('table', {
width: 'calc(100% + 2 * $space$5)',
margin: '0 -$5',
}),
Head: styled('thead', {
position: 'sticky',
top: 0,
backgroundColor: '$black',
'&:after': {
position: 'absolute',
content: '""',
bottom: 0,
left: 0,
right: 0,
borderBottom: '1px solid $slate6',
},
}),
Row: styled('tr'),
Data: styled('td', {
padding: '$3',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}),
Body: styled('tbody', {
tr: {
height: '3rem',
'&:hover': {
cursor: 'pointer',
},
},
}),
},
Skeleton: styled(Skeleton, {
borderRadius: '$lg',
}),
};

View File

@ -0,0 +1,54 @@
import { useNavigate } from 'react-router-dom';
import { Flex, NFAPreview, ResolvedAddress } from '@/components';
import { lastNFAsPaginatedQuery } from '@/graphclient';
import { parseNumberToHexColor } from '@/utils/color';
import { NFAListFragmentStyles as S } from './nfa-list.styles';
type NFARowProps = {
token: lastNFAsPaginatedQuery['tokens'][0];
};
export const NFARow: React.FC<NFARowProps> = ({ token }: NFARowProps) => {
const navigate = useNavigate();
const handleClick = (): void => {
navigate(`/nfa/${token.tokenId}`);
};
return (
<S.Table.Row onClick={handleClick}>
<S.Table.Data>
<Flex
css={{
flexDirection: 'row',
gap: '$2',
alignItems: 'center',
}}
>
<NFAPreview
css={{
borderRadius: '$lg',
borderWidth: '1px',
borderColor: `#${parseNumberToHexColor(token.color)}`,
}}
size="4rem"
name={token.name}
color={`#${parseNumberToHexColor(token.color)}`}
logo={token.logo}
ens={token.ENS}
/>
{token.name}
</Flex>
</S.Table.Data>
<S.Table.Data css={{ textAlign: 'center' }}>
{token.accessPoints?.length ?? 0}
</S.Table.Data>
<S.Table.Data>
{/* TODO add menu button once the component it's added */}
<ResolvedAddress>{token.owner.id}</ResolvedAddress>
</S.Table.Data>
</S.Table.Row>
);
};

View File

@ -0,0 +1,26 @@
import { Flex } from '@/components';
import { NFAListFragmentStyles as S } from './nfa-list.styles';
export const NFARowSkeletonFragment: React.FC = () => (
<S.Table.Row>
<S.Table.Data>
<Flex
css={{
flexDirection: 'row',
gap: '$3',
alignItems: 'center',
}}
>
<S.Skeleton css={{ aspectRatio: 1, width: '5rem' }} />
<S.Skeleton css={{ height: '2rem', width: '100%' }} />
</Flex>
</S.Table.Data>
<S.Table.Data>
<S.Skeleton css={{ height: '2rem' }} />
</S.Table.Data>
<S.Table.Data>
<S.Skeleton css={{ height: '2rem' }} />
</S.Table.Data>
</S.Table.Row>
);

View File

@ -1,33 +1,23 @@
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { NFACard, NFACardSkeleton } from '@/components';
import { lastNFAsPaginatedDocument } from '@/graphclient'; import { lastNFAsPaginatedDocument } from '@/graphclient';
import { useWindowScrollEnd } from '@/hooks'; import { useWindowScrollEnd } from '@/hooks';
import { Explore } from '../explore.context'; import { Explore } from '../../explore.context';
import { NFAListFragmentStyles as S } from './nfa-list.styles'; import { NFAGridFragment } from './nfa-grid.fragment';
import { NFAListFragment } from './nfa-list.fragment';
const pageSize = 10; //Set this size to test pagination const pageSize = 10; //Set this size to test pagination
const LoadingSkeletons: React.FC = () => ( export const NFAsContainerFragment: React.FC = () => {
<>
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
</>
);
export const NFAListFragment: React.FC = () => {
const { const {
endReached, endReached,
orderBy, orderBy,
orderDirection, orderDirection,
pageNumber, pageNumber,
search, search,
nfaView,
setEndReached, setEndReached,
setPageNumber, setPageNumber,
} = Explore.useContext(); } = Explore.useContext();
@ -65,17 +55,9 @@ export const NFAListFragment: React.FC = () => {
if (queryError) return <div>Error</div>; //TODO handle error if (queryError) return <div>Error</div>; //TODO handle error
return ( if (nfaView === 'grid')
<S.Container> return <NFAGridFragment tokens={tokens} isLoading={isLoading} />;
{tokens.map((token) => ( else {
<NFACard data={token} key={token.id} /> return <NFAListFragment tokens={tokens} isLoading={isLoading} />;
))} }
{isLoading && <LoadingSkeletons />}
{!isLoading && tokens.length === 0 && (
<S.EmptyMessage>Nothing found.</S.EmptyMessage>
)}
</S.Container>
);
}; };

View File

@ -1,7 +1,7 @@
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { useState } from 'react'; import { useState } from 'react';
import { Combobox, InputGroup, InputGroupText } from '@/components'; import { Combobox, Flex, Icon, InputGroup, InputGroupText } from '@/components';
import { totalTokensDocument } from '@/graphclient'; import { totalTokensDocument } from '@/graphclient';
import { useDebounce } from '@/hooks'; import { useDebounce } from '@/hooks';
import { FleekERC721 } from '@/integrations/ethereum/contracts'; import { FleekERC721 } from '@/integrations/ethereum/contracts';
@ -25,11 +25,13 @@ const orderResults: SortItem[] = [
export const NFASearchFragment: React.FC = () => { export const NFASearchFragment: React.FC = () => {
const { const {
search, search,
nfaView,
setEndReached, setEndReached,
setOrderBy, setOrderBy,
setOrderDirection, setOrderDirection,
setSearch, setSearch,
setPageNumber, setPageNumber,
setNFAView,
} = Explore.useContext(); } = Explore.useContext();
const [selectedValue, setSelectedValue] = useState<SortItem>(orderResults[0]); const [selectedValue, setSelectedValue] = useState<SortItem>(orderResults[0]);
@ -40,6 +42,10 @@ export const NFASearchFragment: React.FC = () => {
skip: Boolean(search), skip: Boolean(search),
}); });
const handleViewChange = (view: View): void => {
setNFAView(view);
};
const handleSortChange = (item: SortItem | undefined): void => { const handleSortChange = (item: SortItem | undefined): void => {
if (item) { if (item) {
setSelectedValue(item); setSelectedValue(item);
@ -83,41 +89,64 @@ export const NFASearchFragment: React.FC = () => {
return ( return (
<S.Container> <S.Container>
<S.Data.Wrapper> <S.Data.Wrapper>
{totalTokens?.collection && (<> {totalTokens?.collection && (
<S.Data.Text>All NFAs&nbsp;</S.Data.Text> <>
<S.Data.Number>({totalTokens.collection.totalTokens})</S.Data.Number> <S.Data.Text>All NFAs&nbsp;</S.Data.Text>
</>)} <S.Data.Number>
({totalTokens.collection.totalTokens})
</S.Data.Number>
</>
)}
</S.Data.Wrapper> </S.Data.Wrapper>
<S.Input.Wrapper> <S.Flex>
<InputGroup css={{ flex: 1 }}> <S.Input.Wrapper>
<S.Input.Icon name="search" /> <InputGroup css={{ flex: 1 }}>
<InputGroupText placeholder="Search" onChange={handleSearchChange} /> <S.Input.Icon name="search" />
</InputGroup> <InputGroupText
<Combobox placeholder="Search"
items={orderResults} onChange={handleSearchChange}
selected={[selectedValue, handleSortChange]} />
css={{ minWidth: '$28' }} </InputGroup>
queryKey="label" <Combobox
> items={orderResults}
{({ Field, Options }) => ( selected={[selectedValue, handleSortChange]}
<> css={{ minWidth: '$28' }}
<Field queryKey="label"
css={{ >
backgroundColor: '$slate4', {({ Field, Options }) => (
borderColor: '$slate4', <>
color: '$slate11', <Field
}} css={{
> color: '$slate11',
{(selected) => selected?.label || 'Select'} }}
</Field> >
<Options disableSearch css={{ minWidth: '$44', left: 'unset' }}> {(selected) => selected?.label || 'Select'}
{(item) => item.label} </Field>
</Options> <Options disableSearch css={{ minWidth: '$44', left: 'unset' }}>
</> {(item) => item.label}
)} </Options>
</Combobox> </>
</S.Input.Wrapper> )}
</Combobox>
</S.Input.Wrapper>
{/* TODO move this to the app context */}
<S.GridList.Wrapper>
<S.GridList.Icon
name="grid"
selected={nfaView === 'grid'}
css={{ btrr: '0', bbrr: '0' }}
onClick={() => handleViewChange('grid')}
/>
<S.GridList.Icon
name="list"
css={{ btlr: '0', bblr: '0' }}
selected={nfaView === 'list'}
onClick={() => handleViewChange('list')}
/>
</S.GridList.Wrapper>
</S.Flex>
</S.Container> </S.Container>
); );
}; };

View File

@ -25,6 +25,16 @@ export const NFASearchFragmentStyles = {
}), }),
}, },
Flex: styled(Flex, {
flex: 1,
justifyContent: 'flex-end',
gap: '$3h',
'@media (max-width: 374px)': {
flexWrap: 'wrap',
},
}),
Input: { Input: {
Wrapper: styled(Flex, { Wrapper: styled(Flex, {
gap: '$3', gap: '$3',
@ -36,4 +46,31 @@ export const NFASearchFragmentStyles = {
fontSize: '$lg', fontSize: '$lg',
}), }),
}, },
GridList: {
Wrapper: styled(Flex, {
border: '1px solid $slate7',
borderRadius: '$lg',
backgroundColor: '$slate7',
}),
Icon: styled(Icon, {
p: '$2 $3',
border: 'none',
borderRadius: '$lg',
cursor: 'pointer',
variants: {
selected: {
true: {
color: 'white',
backgroundColor: 'transparent',
},
false: {
color: '$slate7 ',
backgroundColor: 'black',
},
},
},
}),
},
}; };

View File

@ -3,17 +3,21 @@ import { useState } from 'react';
import { OrderDirection, Token_orderBy } from '@/graphclient'; import { OrderDirection, Token_orderBy } from '@/graphclient';
import { createContext } from '@/utils'; import { createContext } from '@/utils';
type View = 'grid' | 'list';
export type ExploreContext = { export type ExploreContext = {
search: string; search: string;
orderBy: Token_orderBy; orderBy: Token_orderBy;
orderDirection: OrderDirection; orderDirection: OrderDirection;
pageNumber: number; pageNumber: number;
endReached: boolean; endReached: boolean;
nfaView: View;
setSearch: (search: string) => void; setSearch: (search: string) => void;
setOrderBy: (orderBy: Token_orderBy) => 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;
setNFAView: (view: View) => void;
}; };
const [ExploreProvider, useContext] = createContext<ExploreContext>({ const [ExploreProvider, useContext] = createContext<ExploreContext>({
@ -34,6 +38,7 @@ export abstract class Explore {
useState<OrderDirection>('desc'); useState<OrderDirection>('desc');
const [pageNumber, setPageNumber] = useState(0); const [pageNumber, setPageNumber] = useState(0);
const [endReached, setEndReached] = useState(false); const [endReached, setEndReached] = useState(false);
const [nfaView, setNFAView] = useState<View>('grid');
const context = { const context = {
search, search,
@ -41,11 +46,13 @@ export abstract class Explore {
orderDirection, orderDirection,
pageNumber, pageNumber,
endReached, endReached,
nfaView,
setSearch, setSearch,
setOrderBy, setOrderBy,
setOrderDirection, setOrderDirection,
setPageNumber, setPageNumber,
setEndReached, setEndReached,
setNFAView,
}; };
return <ExploreProvider value={context}>{children}</ExploreProvider>; return <ExploreProvider value={context}>{children}</ExploreProvider>;