-
Notifications
You must be signed in to change notification settings - Fork 396
upcoming: [UIE-9814] - Implement the main product grid with category grouping and load more functionality #13267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
b5a2f63
661f44b
5af99b5
c426d11
00fb001
92a7698
b8d838e
05ec35b
cf854a1
76fbf0f
246f45f
607b82d
0a9638a
9e5c270
f7a924c
e331295
9fa9146
f63bc09
23bafa7
6280ca2
8fcb4b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,175 @@ | ||||||||||||||||
| import { | ||||||||||||||||
harsh-akamai marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||
| useAllMarketplacePartnersMapQuery, | ||||||||||||||||
| useAllMarketplaceTypesMapQuery, | ||||||||||||||||
| useInfiniteMarketplaceProductsQuery, | ||||||||||||||||
| } from '@linode/queries'; | ||||||||||||||||
| import { useTheme } from '@linode/ui'; | ||||||||||||||||
| import { useNavigate } from '@tanstack/react-router'; | ||||||||||||||||
| import * as React from 'react'; | ||||||||||||||||
|
|
||||||||||||||||
| import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; | ||||||||||||||||
|
|
||||||||||||||||
| import { CategorySectionView } from './CategorySectionView'; | ||||||||||||||||
|
|
||||||||||||||||
| import type { ProductCardData } from './ProductSelectionCard'; | ||||||||||||||||
| import type { MarketplaceCategory, MarketplaceProduct } from '@linode/api-v4'; | ||||||||||||||||
|
|
||||||||||||||||
| const INITIAL_DISPLAY_COUNT = 6; | ||||||||||||||||
| const LOAD_MORE_INCREMENT = 6; | ||||||||||||||||
|
|
||||||||||||||||
| export interface CategorySectionProps extends MarketplaceCategory { | ||||||||||||||||
| filteredProducts?: MarketplaceProduct[]; | ||||||||||||||||
| } | ||||||||||||||||
|
||||||||||||||||
| export interface CategorySectionProps extends MarketplaceCategory { | |
| filteredProducts?: MarketplaceProduct[]; | |
| } | |
| export interface CategorySectionProps { | |
| category: MarketplaceCategory; | |
| filters: GlobalFilters; | |
| } |
GlobalFilters coould be like
export interface GlobalFilters {
categoryId?: number;
// IDs derived from search query matching category/type/partner names
searchDerivedCategoryIds?: number[];
searchDerivedPartnerIds?: number[];
searchDerivedTypeIds?: number[];
searchQuery: string;
typeId?: number;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This does sound plausible but when the filters are enabled it would cause us to send a separate product filtered query from each category rendered on the UI. Using one single category agnostic query when the filters are enabled would reduce the number of total api calls that are being made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@harsh-akamai If products api is called without categories and let's assume if filtered products are more then 500, then api will not return all the products in one go
harsh-akamai marked this conversation as resolved.
Show resolved
Hide resolved
harsh-akamai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
pmakode-akamai marked this conversation as resolved.
Show resolved
Hide resolved
pmakode-akamai marked this conversation as resolved.
Show resolved
Hide resolved
harsh-akamai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
harsh-akamai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,182 @@ | ||
| import { Box, Button, ErrorState, Stack, Typography } from '@linode/ui'; | ||
| import { Grid, styled } from '@mui/material'; | ||
| import * as React from 'react'; | ||
|
|
||
| import { SelectionCard } from 'src/components/SelectionCard/SelectionCard'; | ||
| import { Skeleton } from 'src/components/Skeleton'; | ||
|
|
||
| import { ProductSelectionCard, StyledLogoBox } from './ProductSelectionCard'; | ||
|
|
||
| import type { ProductCardItem } from './CategorySection'; | ||
|
|
||
| export interface CategorySectionViewProps { | ||
| cardData: ProductCardItem[]; | ||
| categoryName: string; | ||
| displayCount: number; | ||
| errorMessage: string; | ||
| hasMoreProducts: boolean; | ||
| isFetchingNextPage: boolean; | ||
| isLoading: boolean; | ||
| onLoadMore: () => void; | ||
| onProductClick: (productId: number) => void; | ||
| productsError: boolean; | ||
| skeletonCount: number; | ||
| } | ||
|
|
||
| const MarketplaceSkeletonGrid = ({ | ||
| productsDisplayedCount = 6, | ||
| }: { | ||
| productsDisplayedCount?: number; | ||
| }) => { | ||
| const renderIcon = React.useCallback( | ||
| () => ( | ||
| <Box | ||
| sx={{ | ||
| alignItems: 'flex-start', | ||
|
Check warning on line 35 in packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySectionView.tsx
|
||
| display: 'flex', | ||
| justifyContent: 'space-between', | ||
| width: '100%', | ||
| }} | ||
| > | ||
| { | ||
| <StyledLogoBox> | ||
| <Skeleton height="48px" variant="rounded" width="48px" /> | ||
| </StyledLogoBox> | ||
| } | ||
| </Box> | ||
| ), | ||
| [productsDisplayedCount] | ||
|
Check warning on line 48 in packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySectionView.tsx
|
||
| ); | ||
|
|
||
| const heading = React.useMemo( | ||
| () => <StyledSkeleton width="60%" />, | ||
| [productsDisplayedCount] | ||
|
Check warning on line 53 in packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySectionView.tsx
|
||
| ); | ||
|
|
||
| const subHeadings = React.useMemo( | ||
| () => [ | ||
| <StyledSkeleton key="company" width="40%" />, | ||
| <Box | ||
| key="description" | ||
| sx={(theme) => ({ | ||
| marginTop: theme.spacingFunction(12), | ||
| paddingBottom: theme.spacingFunction(36), // Always space for type chip at bottom | ||
| })} | ||
| > | ||
| <StyledSkeleton /> | ||
| <StyledSkeleton /> | ||
| <StyledSkeleton width="50%" /> | ||
| </Box>, | ||
| <Box | ||
| key="category" | ||
| sx={(theme) => ({ | ||
| bottom: theme.spacingFunction(16), | ||
| left: theme.spacingFunction(20), | ||
| position: 'absolute', | ||
| })} | ||
| > | ||
| <StyledSkeleton height={20} width={80} /> | ||
| </Box>, | ||
| ], | ||
| [productsDisplayedCount] | ||
|
Check warning on line 81 in packages/manager/src/features/Marketplace/MarketplaceLanding/CategorySectionView.tsx
|
||
| ); | ||
| return ( | ||
| <Grid container spacing={2}> | ||
| {Array.from({ length: productsDisplayedCount }).map((_) => ( | ||
| <SelectionCard | ||
| heading={heading} | ||
| key="skeleton card" | ||
harsh-akamai marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| renderIcon={renderIcon} | ||
| subheadings={subHeadings} | ||
| sxCardBase={(theme) => ({ | ||
| alignItems: 'flex-start', | ||
| flexDirection: 'column', | ||
| minHeight: '280px', | ||
| padding: `${theme.spacingFunction(16)} ${theme.spacingFunction(20)}`, | ||
| position: 'relative', | ||
| gap: theme.spacingFunction(12), | ||
| '&:hover': { | ||
| borderColor: theme.borderColors.divider, | ||
| }, | ||
| })} | ||
| sxCardBaseHeading={{ width: '100%' }} | ||
| sxCardBaseIcon={{ | ||
| alignItems: 'flex-start', | ||
| justifyContent: 'flex-start', | ||
| width: '100%', | ||
| }} | ||
| /> | ||
| ))} | ||
| </Grid> | ||
| ); | ||
| }; | ||
|
|
||
| const ProductsGrid = ({ | ||
| cardData, | ||
| onProductClick, | ||
| }: { | ||
| cardData: ProductCardItem[]; | ||
| onProductClick: (productId: number) => void; | ||
| }) => ( | ||
| <Grid container spacing={2}> | ||
| {cardData.map((item) => ( | ||
| <ProductSelectionCard | ||
| data={item.data} | ||
| key={item.id} | ||
| onClick={() => onProductClick(item.id)} | ||
| /> | ||
| ))} | ||
| </Grid> | ||
| ); | ||
|
|
||
| export const CategorySectionView = (props: CategorySectionViewProps) => { | ||
| const { | ||
| categoryName, | ||
| isLoading, | ||
| isFetchingNextPage, | ||
| hasMoreProducts, | ||
| productsError, | ||
| displayCount, | ||
| cardData, | ||
| skeletonCount, | ||
| errorMessage, | ||
| onLoadMore, | ||
| onProductClick, | ||
| } = props; | ||
|
|
||
| if (productsError) { | ||
| return <ErrorState errorText={errorMessage} />; | ||
| } | ||
|
|
||
| return ( | ||
| <Stack spacing={2}> | ||
| <Typography variant="h2">{categoryName}</Typography> | ||
|
|
||
| {isLoading ? ( | ||
| <MarketplaceSkeletonGrid productsDisplayedCount={displayCount} /> | ||
| ) : ( | ||
| <ProductsGrid cardData={cardData} onProductClick={onProductClick} /> | ||
| )} | ||
|
|
||
| {isFetchingNextPage && ( | ||
| <MarketplaceSkeletonGrid productsDisplayedCount={skeletonCount} /> | ||
| )} | ||
|
|
||
| {!isFetchingNextPage && hasMoreProducts && ( | ||
| <Button | ||
| onClick={onLoadMore} | ||
| sx={{ | ||
| justifyContent: 'start', | ||
| paddingLeft: 0, | ||
| }} | ||
| > | ||
| Load More... | ||
| </Button> | ||
| )} | ||
| </Stack> | ||
| ); | ||
| }; | ||
|
|
||
| const StyledSkeleton = styled(Skeleton)({ | ||
| borderRadius: '4px', | ||
| }); | ||
Uh oh!
There was an error while loading. Please reload this page.