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 {
id
}
owner {
id
}
verified
}
}

View File

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

View File

@ -1,6 +1,7 @@
import { AiFillCheckCircle } from '@react-icons/all-files/ai/AiFillCheckCircle';
import { AiOutlineCheck } from '@react-icons/all-files/ai/AiOutlineCheck';
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 { BiSearch } from '@react-icons/all-files/bi/BiSearch';
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 { IoClose } from '@react-icons/all-files/io5/IoClose';
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 { IoLogoGithub } from '@react-icons/all-files/io5/IoLogoGithub';
import { MdVerifiedUser } from '@react-icons/all-files/md/MdVerifiedUser';
@ -43,7 +45,9 @@ export const IconLibrary = Object.freeze({
'fleek-logo': FleekLogo,
'fleek-name': FleekName,
github: IoLogoGithub,
grid: IoGridOutline,
info: IoInformationCircleSharp,
list: AiOutlineUnorderedList,
menu: FaBars,
metamask: MetamaskIcon, //remove if not used
opensea: OpenseaIcon,

View File

@ -1,12 +1,12 @@
import { Explore } from '../explore.context';
import { NFAListFragment } from './nfa-list.fragment';
import { NFAsContainerFragment } from './nfa-list';
import { NFASearchFragment } from './nfa-search.fragment';
export const ExploreListFragment: React.FC = () => {
return (
<Explore.Provider>
<NFASearchFragment />
<NFAListFragment />
<NFAsContainerFragment />
</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 { useEffect } from 'react';
import { NFACard, NFACardSkeleton } from '@/components';
import { lastNFAsPaginatedDocument } from '@/graphclient';
import { useWindowScrollEnd } from '@/hooks';
import { Explore } from '../explore.context';
import { NFAListFragmentStyles as S } from './nfa-list.styles';
import { Explore } from '../../explore.context';
import { NFAGridFragment } from './nfa-grid.fragment';
import { NFAListFragment } from './nfa-list.fragment';
const pageSize = 10; //Set this size to test pagination
const LoadingSkeletons: React.FC = () => (
<>
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
<NFACardSkeleton />
</>
);
export const NFAListFragment: React.FC = () => {
export const NFAsContainerFragment: React.FC = () => {
const {
endReached,
orderBy,
orderDirection,
pageNumber,
search,
nfaView,
setEndReached,
setPageNumber,
} = Explore.useContext();
@ -65,17 +55,9 @@ export const NFAListFragment: React.FC = () => {
if (queryError) return <div>Error</div>; //TODO handle error
return (
<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>
);
if (nfaView === 'grid')
return <NFAGridFragment tokens={tokens} isLoading={isLoading} />;
else {
return <NFAListFragment tokens={tokens} isLoading={isLoading} />;
}
};

View File

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

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