-
Notifications
You must be signed in to change notification settings - Fork 581
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(DIA-931): implement the chips and footer of collections by categ…
…ory screen (#10970) feat: implement the chips and footer of collections by category screen
- Loading branch information
1 parent
07eb92c
commit 54064c9
Showing
10 changed files
with
429 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import { Flex, Skeleton, SkeletonText, Text, useSpace } from "@artsy/palette-mobile" | ||
import { useRoute } from "@react-navigation/native" | ||
import { BodyCollectionsByCategoryQuery } from "__generated__/BodyCollectionsByCategoryQuery.graphql" | ||
import { CollectionsChips_marketingCollections$key } from "__generated__/CollectionsChips_marketingCollections.graphql" | ||
import { CollectionsByCategoriesRouteProp } from "app/Scenes/CollectionsByCategory/CollectionsByCategory" | ||
import { | ||
CollectionsChips, | ||
CollectionsChipsPlaceholder, | ||
} from "app/Scenes/CollectionsByCategory/CollectionsChips" | ||
import { NoFallback, withSuspense } from "app/utils/hooks/withSuspense" | ||
import { graphql, useLazyLoadQuery } from "react-relay" | ||
|
||
interface BodyProps { | ||
marketingCollections: CollectionsChips_marketingCollections$key | ||
} | ||
|
||
export const Body: React.FC<BodyProps> = ({ marketingCollections }) => { | ||
const space = useSpace() | ||
const { params } = useRoute<CollectionsByCategoriesRouteProp>() | ||
const category = params.props.category | ||
|
||
return ( | ||
<Flex px={2} gap={space(2)}> | ||
<Text variant="xl">{category}</Text> | ||
|
||
<Flex gap={space(1)}> | ||
<Text>Explore collections with {category}</Text> | ||
<CollectionsChips marketingCollections={marketingCollections} /> | ||
</Flex> | ||
</Flex> | ||
) | ||
} | ||
|
||
const BodyPlaceholder: React.FC = () => { | ||
const space = useSpace() | ||
|
||
return ( | ||
<Skeleton> | ||
<Flex px={2} gap={space(2)}> | ||
<SkeletonText variant="xl">Category</SkeletonText> | ||
|
||
<Flex gap={space(1)}> | ||
<SkeletonText>Category description text</SkeletonText> | ||
|
||
<CollectionsChipsPlaceholder /> | ||
</Flex> | ||
</Flex> | ||
</Skeleton> | ||
) | ||
} | ||
|
||
const query = graphql` | ||
query BodyCollectionsByCategoryQuery($category: String!) { | ||
marketingCollections(category: $category, size: 10) { | ||
...CollectionsChips_marketingCollections | ||
} | ||
} | ||
` | ||
|
||
export const BodyWithSuspense = withSuspense({ | ||
Component: () => { | ||
const { params } = useRoute<CollectionsByCategoriesRouteProp>() | ||
const data = useLazyLoadQuery<BodyCollectionsByCategoryQuery>(query, { | ||
category: params.props.entityID, | ||
}) | ||
|
||
if (!data.marketingCollections) { | ||
return <BodyPlaceholder /> | ||
} | ||
|
||
return <Body marketingCollections={data.marketingCollections} /> | ||
}, | ||
LoadingFallback: BodyPlaceholder, | ||
ErrorFallback: NoFallback, | ||
}) |
32 changes: 24 additions & 8 deletions
32
src/app/Scenes/CollectionsByCategory/CollectionsByCategory.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
src/app/Scenes/CollectionsByCategory/CollectionsChips.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
import { Chip, Flex, SkeletonBox, SkeletonText, Spacer, useSpace } from "@artsy/palette-mobile" | ||
import { CollectionsChips_marketingCollections$key } from "__generated__/CollectionsChips_marketingCollections.graphql" | ||
import { navigate } from "app/system/navigation/navigate" | ||
import { Dimensions, FlatList, ScrollView } from "react-native" | ||
import { isTablet } from "react-native-device-info" | ||
import { graphql, useFragment } from "react-relay" | ||
|
||
const { width } = Dimensions.get("window") | ||
const CHIP_WIDTH = 260 | ||
|
||
interface CollectionsChipsProps { | ||
marketingCollections: CollectionsChips_marketingCollections$key | ||
} | ||
|
||
export const CollectionsChips: React.FC<CollectionsChipsProps> = ({ | ||
marketingCollections: _marketingCollections, | ||
}) => { | ||
const marketingCollections = useFragment(fragment, _marketingCollections) | ||
const space = useSpace() | ||
|
||
if (!marketingCollections) { | ||
return null | ||
} | ||
|
||
const numColumns = Math.ceil(marketingCollections.length / 3) | ||
|
||
const snapToOffsets = getSnapToOffsets(numColumns, space(1), space(2)) | ||
|
||
return ( | ||
<Flex> | ||
<ScrollView | ||
horizontal | ||
showsHorizontalScrollIndicator={false} | ||
pagingEnabled | ||
snapToEnd={false} | ||
snapToOffsets={snapToOffsets} | ||
decelerationRate="fast" | ||
> | ||
<FlatList | ||
scrollEnabled={false} | ||
columnWrapperStyle={numColumns > 1 ? { gap: space(1) } : undefined} | ||
ItemSeparatorComponent={() => <Spacer y={1} />} | ||
showsVerticalScrollIndicator={false} | ||
showsHorizontalScrollIndicator={false} | ||
numColumns={numColumns} | ||
data={marketingCollections} | ||
keyExtractor={(item, index) => `item_${index}_${item.internalID}`} | ||
renderItem={({ item }) => ( | ||
<Flex minWidth={CHIP_WIDTH}> | ||
<Chip | ||
key={item.internalID} | ||
title={item.title} | ||
onPress={() => { | ||
if (item?.slug) navigate(`/collection/${item.slug}`) | ||
}} | ||
/> | ||
</Flex> | ||
)} | ||
/> | ||
</ScrollView> | ||
</Flex> | ||
) | ||
} | ||
|
||
const fragment = graphql` | ||
fragment CollectionsChips_marketingCollections on MarketingCollection @relay(plural: true) { | ||
internalID | ||
title | ||
slug | ||
} | ||
` | ||
|
||
export const CollectionsChipsPlaceholder: React.FC = () => { | ||
const space = useSpace() | ||
const size = 6 | ||
const numColumns = !isTablet() ? Math.ceil(size / 3) : Math.ceil(size / 2) | ||
|
||
return ( | ||
<Flex> | ||
<ScrollView horizontal showsHorizontalScrollIndicator={false} scrollEnabled={false}> | ||
<FlatList | ||
scrollEnabled={false} | ||
data={Array.from({ length: size })} | ||
columnWrapperStyle={{ gap: space(1) }} | ||
showsVerticalScrollIndicator={false} | ||
showsHorizontalScrollIndicator={false} | ||
numColumns={numColumns} | ||
ItemSeparatorComponent={() => <Spacer y={1} />} | ||
renderItem={() => ( | ||
<SkeletonBox width={CHIP_WIDTH} height={70}> | ||
<SkeletonText>Collection</SkeletonText> | ||
</SkeletonBox> | ||
)} | ||
/> | ||
</ScrollView> | ||
</Flex> | ||
) | ||
} | ||
|
||
const getSnapToOffsets = (numColumns: number, gap: number, padding: number) => { | ||
if (!isTablet()) { | ||
// first and last elements are cornered | ||
const firstOffset = CHIP_WIDTH + gap + CHIP_WIDTH / 2 - (width / 2 - padding) | ||
const lastOffset = CHIP_WIDTH * (numColumns - 1) | ||
// the middle elements are centered, the logic here is | ||
// first element offset + CHIP_WIDTH + gap multiplied by the index to keep it increasing | ||
const middleOffsets = Array.from({ length: numColumns - 2 }).map((_, index) => { | ||
const offset = (CHIP_WIDTH + gap) * (index + 1) | ||
return firstOffset + offset | ||
}) | ||
return [firstOffset, ...middleOffsets, lastOffset] | ||
} | ||
|
||
return [CHIP_WIDTH * numColumns - 2] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { Flex, Skeleton, SkeletonText, Text, Touchable, useSpace } from "@artsy/palette-mobile" | ||
import { useRoute } from "@react-navigation/native" | ||
import { FooterCollectionsByCategoryQuery } from "__generated__/FooterCollectionsByCategoryQuery.graphql" | ||
import { Footer_homeViewSectionCards$key } from "__generated__/Footer_homeViewSectionCards.graphql" | ||
import { CollectionsByCategoriesRouteProp } from "app/Scenes/CollectionsByCategory/CollectionsByCategory" | ||
import { navigate } from "app/system/navigation/navigate" | ||
import { extractNodes } from "app/utils/extractNodes" | ||
import { NoFallback, withSuspense } from "app/utils/hooks/withSuspense" | ||
import { FC } from "react" | ||
import { graphql, useLazyLoadQuery, useFragment } from "react-relay" | ||
|
||
interface FooterProps { | ||
cards: Footer_homeViewSectionCards$key | ||
homeViewSectionId: string | ||
} | ||
|
||
export const Footer: FC<FooterProps> = ({ cards, homeViewSectionId }) => { | ||
const { params } = useRoute<CollectionsByCategoriesRouteProp>() | ||
const data = useFragment(fragment, cards) | ||
const space = useSpace() | ||
|
||
const category = decodeURI(params.props.category) | ||
|
||
const categories = extractNodes(data?.cardsConnection).filter((c) => c.title !== category) | ||
|
||
if (!data || categories.length === 0) { | ||
return null | ||
} | ||
|
||
const handleCategoryPress = (category: string, entityID: string) => { | ||
navigate( | ||
`/collections-by-category/${category}?homeViewSectionId=${homeViewSectionId}&entityID=${entityID}` | ||
) | ||
} | ||
|
||
return ( | ||
<Flex backgroundColor="black100" p={2} gap={space(2)}> | ||
<Text color="white100">Explore more categories</Text> | ||
|
||
{categories.map((c, index) => ( | ||
<Touchable | ||
key={`category_rail_${index}`} | ||
onPress={() => handleCategoryPress(c.title, c.entityID)} | ||
> | ||
<Text variant="xl" color="white100"> | ||
{c.title} | ||
</Text> | ||
</Touchable> | ||
))} | ||
</Flex> | ||
) | ||
} | ||
|
||
const fragment = graphql` | ||
fragment Footer_homeViewSectionCards on HomeViewSectionCards { | ||
cardsConnection(first: 6) { | ||
edges { | ||
node { | ||
title @required(action: NONE) | ||
entityID @required(action: NONE) | ||
} | ||
} | ||
} | ||
} | ||
` | ||
|
||
const FooterPlaceholder: FC = () => { | ||
const space = useSpace() | ||
|
||
return ( | ||
<Skeleton> | ||
<Flex p={2} gap={space(2)}> | ||
<SkeletonText>Explore more categories</SkeletonText> | ||
|
||
{Array.from({ length: 5 }).map((_, index) => ( | ||
<SkeletonText key={`category_rail_${index}`} variant="xl"> | ||
Category | ||
</SkeletonText> | ||
))} | ||
</Flex> | ||
</Skeleton> | ||
) | ||
} | ||
|
||
const query = graphql` | ||
query FooterCollectionsByCategoryQuery($id: String!) { | ||
homeView { | ||
section(id: $id) { | ||
...Footer_homeViewSectionCards | ||
} | ||
} | ||
} | ||
` | ||
|
||
export const FooterWithSuspense = withSuspense<Pick<FooterProps, "homeViewSectionId">>({ | ||
Component: ({ homeViewSectionId }) => { | ||
const data = useLazyLoadQuery<FooterCollectionsByCategoryQuery>(query, { | ||
id: homeViewSectionId, | ||
}) | ||
|
||
if (!data?.homeView.section) { | ||
return <FooterPlaceholder /> | ||
} | ||
|
||
return <Footer cards={data.homeView.section} homeViewSectionId={homeViewSectionId} /> | ||
}, | ||
LoadingFallback: FooterPlaceholder, | ||
ErrorFallback: NoFallback, | ||
}) |
24 changes: 24 additions & 0 deletions
24
src/app/Scenes/CollectionsByCategory/__tests__/CollectionsChips.tests.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import { screen } from "@testing-library/react-native" | ||
import { CollectionsChipsTestQuery } from "__generated__/CollectionsChipsTestQuery.graphql" | ||
import { CollectionsChips } from "app/Scenes/CollectionsByCategory/CollectionsChips" | ||
import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" | ||
import { graphql } from "react-relay" | ||
|
||
describe("CollectionsChips", () => { | ||
const { renderWithRelay } = setupTestWrapper<CollectionsChipsTestQuery>({ | ||
Component: CollectionsChips, | ||
query: graphql` | ||
query CollectionsChipsTestQuery { | ||
marketingCollections(category: "test", size: 10) @required(action: NONE) { | ||
...CollectionsChips_marketingCollections | ||
} | ||
} | ||
`, | ||
}) | ||
|
||
it("renders", () => { | ||
renderWithRelay() | ||
|
||
expect(screen.getByText(/mock-value-for-field-"title"/)).toBeOnTheScreen() | ||
}) | ||
}) |
Oops, something went wrong.