Commit 60463634 authored by tom's avatar tom

skeletons for marketplace

parent e24ac42e
import type { NextPage } from 'next';
import dynamic from 'next/dynamic';
import Head from 'next/head';
import React from 'react';
import Marketplace from 'ui/pages/Marketplace';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
const Marketplace = dynamic(() => import('ui/pages/Marketplace'), { ssr: false });
const MarketplacePage: NextPage = () => {
return (
<Page>
......
/* eslint-disable max-len */
import type { MarketplaceAppOverview } from 'types/client/marketplace';
export const MARKETPLACE_APP: MarketplaceAppOverview = {
author: 'StubApp Inc.',
id: 'stub-app',
title: 'My cool app name',
logo: '',
categories: [
'Bridge',
],
shortDescription: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.',
site: 'https://example.com',
description: 'Hop is a scalable rollup-to-rollup general token bridge. It allows users to send tokens from one rollup or sidechain to another almost immediately without having to wait for the networks challenge period.',
external: true,
url: 'https://example.com',
};
import { Box, Heading, Icon, IconButton, Image, Link, LinkBox, Text, useColorModeValue } from '@chakra-ui/react';
import { Box, Icon, IconButton, Image, Link, LinkBox, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { MouseEvent } from 'react';
import React, { useCallback } from 'react';
......@@ -14,6 +14,7 @@ interface Props extends MarketplaceAppPreview {
onInfoClick: (id: string) => void;
isFavorite: boolean;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
}
const MarketplaceAppCard = ({
......@@ -28,6 +29,7 @@ const MarketplaceAppCard = ({
onInfoClick,
isFavorite,
onFavoriteClick,
isLoading,
}: Props) => {
const categoriesLabel = categories.join(', ');
......@@ -41,14 +43,15 @@ const MarketplaceAppCard = ({
}, [ onFavoriteClick, id, isFavorite ]);
const logoUrl = useColorModeValue(logo, logoDarkMode || logo);
const moreButtonBgGradient = `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)`;
return (
<LinkBox
_hover={{
boxShadow: 'md',
boxShadow: isLoading ? 'none' : 'md',
}}
_focusWithin={{
boxShadow: 'md',
boxShadow: isLoading ? 'none' : 'md',
}}
borderRadius="md"
height="100%"
......@@ -60,12 +63,13 @@ const MarketplaceAppCard = ({
<Box
display={{ base: 'grid', sm: 'block' }}
gridTemplateColumns={{ base: '64px 1fr', sm: '1fr' }}
gridTemplateRows={{ base: '20px 20px auto', sm: 'none' }}
gridTemplateRows={{ base: 'none', sm: 'none' }}
gridRowGap={{ base: 2, sm: 'none' }}
gridColumnGap={{ base: 4, sm: 'none' }}
height="100%"
>
<Box
<Skeleton
isLoaded={ !isLoading }
gridRow={{ base: '1 / 4', sm: 'auto' }}
marginBottom={ 4 }
w={{ base: '64px', sm: '96px' }}
......@@ -76,17 +80,20 @@ const MarketplaceAppCard = ({
justifyContent="center"
>
<Image
src={ logoUrl }
src={ isLoading ? undefined : logoUrl }
alt={ `${ title } app icon` }
/>
</Box>
</Skeleton>
<Heading
<Skeleton
isLoaded={ !isLoading }
gridColumn={{ base: 2, sm: 'auto' }}
as="h3"
marginBottom={ 2 }
size={{ base: 'xs', sm: 'sm' }}
marginBottom={{ base: 0, sm: 2 }}
fontSize={{ base: 'sm', sm: 'lg' }}
fontWeight="semibold"
fontFamily="heading"
display="inline-block"
>
<MarketplaceAppCardLink
id={ id }
......@@ -94,30 +101,33 @@ const MarketplaceAppCard = ({
external={ external }
title={ title }
/>
</Heading>
</Skeleton>
<Text
marginBottom={ 2 }
variant="secondary"
<Skeleton
isLoaded={ !isLoading }
marginBottom={{ base: 0, sm: 2 }}
color="text_secondary"
fontSize="xs"
>
{ categoriesLabel }
</Text>
<span>{ categoriesLabel }</span>
</Skeleton>
<Text
<Skeleton
isLoaded={ !isLoading }
fontSize={{ base: 'xs', sm: 'sm' }}
lineHeight="20px"
noOfLines={ 4 }
>
{ shortDescription }
</Text>
</Skeleton>
{ !isLoading && (
<Box
position="absolute"
right={{ base: 3, sm: '20px' }}
bottom={{ base: 3, sm: '20px' }}
paddingLeft={ 8 }
bgGradient={ `linear(to-r, ${ useColorModeValue('whiteAlpha.50', 'blackAlpha.50') }, ${ useColorModeValue('white', 'black') } 20%)` }
bgGradient={ moreButtonBgGradient }
>
<Link
fontSize={{ base: 'xs', sm: 'sm' }}
......@@ -137,7 +147,9 @@ const MarketplaceAppCard = ({
/>
</Link>
</Box>
) }
{ !isLoading && (
<IconButton
display={{ base: 'block', sm: isFavorite ? 'block' : 'none' }}
_groupHover={{ display: 'block' }}
......@@ -156,6 +168,7 @@ const MarketplaceAppCard = ({
<Icon as={ starOutlineIcon } w={ 4 } h={ 4 } color="gray.300"/>
}
/>
) }
</Box>
</LinkBox>
);
......
import { Box, Button, Icon, Menu, MenuButton, MenuList } from '@chakra-ui/react';
import { Box, Button, Icon, Menu, MenuButton, MenuList, Skeleton } from '@chakra-ui/react';
import React from 'react';
import { MarketplaceCategory } from 'types/client/marketplace';
......@@ -11,15 +11,28 @@ type Props = {
categories: Array<string>;
selectedCategoryId: string;
onSelect: (category: string) => void;
isLoading: boolean;
}
const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories }: Props) => {
const MarketplaceCategoriesMenu = ({ selectedCategoryId, onSelect, categories, isLoading }: Props) => {
const options = React.useMemo(() => ([
MarketplaceCategory.FAVORITES,
MarketplaceCategory.ALL,
...categories,
]), [ categories ]);
if (isLoading) {
return (
<Skeleton
h="40px"
w={{ base: '100%', sm: '120px' }}
borderRadius="base"
mb={{ base: 2, sm: 0 }}
mr={{ base: 0, sm: 2 }}
/>
);
}
return (
<Menu>
<MenuButton
......
import { Grid, GridItem } from '@chakra-ui/react';
import { Grid } from '@chakra-ui/react';
import React from 'react';
import type { MarketplaceAppPreview } from 'types/client/marketplace';
......@@ -13,9 +13,10 @@ type Props = {
onAppClick: (id: string) => void;
favoriteApps: Array<string>;
onFavoriteClick: (id: string, isFavorite: boolean) => void;
isLoading: boolean;
}
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Props) => {
const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick, isLoading }: Props) => {
return apps.length > 0 ? (
<Grid
templateColumns={{
......@@ -25,11 +26,9 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Pr
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ apps.map((app) => (
<GridItem
key={ app.id }
>
{ apps.map((app, index) => (
<MarketplaceAppCard
key={ app.id + (isLoading ? index : '') }
onInfoClick={ onAppClick }
id={ app.id }
external={ app.external }
......@@ -41,8 +40,8 @@ const MarketplaceList = ({ apps, onAppClick, favoriteApps, onFavoriteClick }: Pr
categories={ app.categories }
isFavorite={ favoriteApps.includes(app.id) }
onFavoriteClick={ onFavoriteClick }
isLoading={ isLoading }
/>
</GridItem>
)) }
</Grid>
) : (
......
import { Grid, GridItem } from '@chakra-ui/react';
import React from 'react';
import MarketplaceAppCardSkeleton from './MarketplaceAppCardSkeleton';
const applicationStubs = [ ...Array(12) ];
const MarketplaceListSkeleton = () => {
return (
<Grid
templateColumns={{
sm: 'repeat(auto-fill, minmax(170px, 1fr))',
lg: 'repeat(auto-fill, minmax(260px, 1fr))',
}}
autoRows="1fr"
gap={{ base: '16px', sm: '24px' }}
>
{ applicationStubs.map((app, index) => (
<GridItem
key={ index }
>
<MarketplaceAppCardSkeleton/>
</GridItem>
)) }
</Grid>
);
};
export default MarketplaceListSkeleton;
......@@ -12,6 +12,7 @@ import type { ResourceError } from 'lib/api/resources';
import useDebounce from 'lib/hooks/useDebounce';
import useApiFetch from 'lib/hooks/useFetch';
import getQueryParamString from 'lib/router/getQueryParamString';
import { MARKETPLACE_APP } from 'stubs/marketplace';
const favoriteAppsLocalStorageKey = 'favoriteApps';
......@@ -44,11 +45,12 @@ export default function useMarketplace() {
const [ favoriteApps, setFavoriteApps ] = React.useState<Array<string>>([]);
const apiFetch = useApiFetch();
const { isLoading, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
const { isPlaceholderData, isError, error, data } = useQuery<unknown, ResourceError<unknown>, Array<MarketplaceAppOverview>>(
[ 'marketplace-apps' ],
async() => apiFetch(appConfig.marketplaceConfigUrl || ''),
{
select: (data) => (data as Array<MarketplaceAppOverview>).sort((a, b) => a.title.localeCompare(b.title)),
placeholderData: Array(9).fill(MARKETPLACE_APP),
staleTime: Infinity,
});
......@@ -90,13 +92,13 @@ export default function useMarketplace() {
}, [ ]);
React.useEffect(() => {
if (!isLoading && !isError) {
if (!isPlaceholderData && !isError) {
const isValidDefaultCategory = categories.includes(defaultCategoryId);
isValidDefaultCategory && setSelectedCategoryId(defaultCategoryId);
}
// run only when data is loaded
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ isLoading ]);
}, [ isPlaceholderData ]);
React.useEffect(() => {
const query = _pickBy({
......@@ -118,7 +120,7 @@ export default function useMarketplace() {
onCategoryChange: handleCategoryChange,
filterQuery: debouncedFilterQuery,
onSearchInputChange: setFilterQuery,
isLoading,
isPlaceholderData,
isError,
error,
categories,
......@@ -139,7 +141,7 @@ export default function useMarketplace() {
handleCategoryChange,
handleFavoriteClick,
isError,
isLoading,
isPlaceholderData,
showAppInfo,
debouncedFilterQuery,
]);
......
import { Box, Icon, Link } from '@chakra-ui/react';
import { Box, Icon, Link, Skeleton } from '@chakra-ui/react';
import React from 'react';
import config from 'configs/app/config';
......@@ -6,14 +6,13 @@ import PlusIcon from 'icons/plus.svg';
import MarketplaceAppModal from 'ui/marketplace/MarketplaceAppModal';
import MarketplaceCategoriesMenu from 'ui/marketplace/MarketplaceCategoriesMenu';
import MarketplaceList from 'ui/marketplace/MarketplaceList';
import MarketplaceListSkeleton from 'ui/marketplace/MarketplaceListSkeleton';
import FilterInput from 'ui/shared/filters/FilterInput';
import useMarketplace from '../marketplace/useMarketplace';
const Marketplace = () => {
const {
isLoading,
isPlaceholderData,
isError,
error,
selectedCategoryId,
......@@ -45,24 +44,26 @@ const Marketplace = () => {
categories={ categories }
selectedCategoryId={ selectedCategoryId }
onSelect={ onCategoryChange }
isLoading={ isPlaceholderData }
/>
<FilterInput
initialValue={ filterQuery }
onChange={ onSearchInputChange }
marginBottom={{ base: '4', lg: '6' }}
w="100%"
placeholder="Find app"
isLoading={ isPlaceholderData }
/>
</Box>
{ isLoading ? <MarketplaceListSkeleton/> : (
<MarketplaceList
apps={ displayedApps }
onAppClick={ showAppInfo }
favoriteApps={ favoriteApps }
onFavoriteClick={ onFavoriteClick }
isLoading={ isPlaceholderData }
/>
) }
{ selectedApp && (
<MarketplaceAppModal
......@@ -74,11 +75,15 @@ const Marketplace = () => {
) }
{ config.marketplaceSubmitForm && (
<Skeleton
isLoaded={ !isPlaceholderData }
marginTop={{ base: 8, sm: 16 }}
display="inline-block"
>
<Link
fontWeight="bold"
display="inline-flex"
alignItems="baseline"
marginTop={{ base: 8, sm: 16 }}
href={ config.marketplaceSubmitForm }
isExternal
>
......@@ -91,6 +96,7 @@ const Marketplace = () => {
Submit an App
</Link>
</Skeleton>
) }
</>
);
......
import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, useColorModeValue } from '@chakra-ui/react';
import { chakra, Icon, Input, InputGroup, InputLeftElement, InputRightElement, Skeleton, useColorModeValue } from '@chakra-ui/react';
import type { ChangeEvent } from 'react';
import React, { useCallback, useState } from 'react';
......@@ -11,9 +11,10 @@ type Props = {
size?: 'xs' | 'sm' | 'md' | 'lg';
placeholder: string;
initialValue?: string;
isLoading?: boolean;
}
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue }: Props) => {
const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialValue, isLoading }: Props) => {
const [ filterQuery, setFilterQuery ] = useState(initialValue || '');
const inputRef = React.useRef<HTMLInputElement>(null);
const iconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
......@@ -32,10 +33,13 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
}, [ onChange ]);
return (
<InputGroup
size={ size }
<Skeleton
isLoaded={ !isLoading }
className={ className }
minW="250px"
>
<InputGroup
size={ size }
>
<InputLeftElement
pointerEvents="none"
......@@ -60,6 +64,7 @@ const FilterInput = ({ onChange, className, size = 'sm', placeholder, initialVal
</InputRightElement>
) : null }
</InputGroup>
</Skeleton>
);
};
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment