Skip to content

Commit

Permalink
feat(DIA-931): implement the chips and footer of collections by categ…
Browse files Browse the repository at this point in the history
…ory screen (#10970)

feat: implement the chips and footer of collections by category screen
  • Loading branch information
araujobarret authored Oct 17, 2024
1 parent 07eb92c commit 54064c9
Show file tree
Hide file tree
Showing 10 changed files with 429 additions and 22 deletions.
75 changes: 75 additions & 0 deletions src/app/Scenes/CollectionsByCategory/Body.tsx
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 src/app/Scenes/CollectionsByCategory/CollectionsByCategory.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,40 @@
import { Flex, Screen, Text } from "@artsy/palette-mobile"
import { Flex, Screen, useSpace } from "@artsy/palette-mobile"
import { RouteProp, useRoute } from "@react-navigation/native"
import { BodyWithSuspense } from "app/Scenes/CollectionsByCategory/Body"
import { FooterWithSuspense } from "app/Scenes/CollectionsByCategory/Footer"
import { goBack } from "app/system/navigation/navigate"
import { FC } from "react"

type CollectionsByCategoriesNavigationRoutes = {
collections: { props: { category: string } }
collections: {
props: {
category: string
entityID: string
homeViewSectionId: string
}
}
}

export type CollectionsByCategoriesRouteProp = RouteProp<
CollectionsByCategoriesNavigationRoutes,
"collections"
>

export const CollectionsByCategory: FC = () => {
const { params } = useRoute<RouteProp<CollectionsByCategoriesNavigationRoutes, "collections">>()
const { params } = useRoute<CollectionsByCategoriesRouteProp>()
const space = useSpace()

const category = params?.props.category ?? ""
const category = params.props.category

return (
<Screen>
<Screen.Header onBack={goBack} title="Collections" animated />
<Screen.Body fullwidth px={2}>
<Screen.Header onBack={goBack} title={category} animated />
<Screen.Body fullwidth flex={1}>
<Screen.ScrollView>
<Flex>
<Text>{category}</Text>
<Flex gap={space(4)}>
<BodyWithSuspense />

<FooterWithSuspense homeViewSectionId={params.props.homeViewSectionId} />
</Flex>
</Screen.ScrollView>
</Screen.Body>
Expand Down
115 changes: 115 additions & 0 deletions src/app/Scenes/CollectionsByCategory/CollectionsChips.tsx
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]
}
109 changes: 109 additions & 0 deletions src/app/Scenes/CollectionsByCategory/Footer.tsx
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,
})
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()
})
})
Loading

0 comments on commit 54064c9

Please sign in to comment.