From 54064c9ddb544ba6d354eebe1e2ca349a68d911b Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 17 Oct 2024 18:39:02 +0200 Subject: [PATCH] feat(DIA-931): implement the chips and footer of collections by category screen (#10970) feat: implement the chips and footer of collections by category screen --- src/app/Scenes/CollectionsByCategory/Body.tsx | 75 ++++++++++++ .../CollectionsByCategory.tsx | 32 +++-- .../CollectionsChips.tsx | 115 ++++++++++++++++++ .../Scenes/CollectionsByCategory/Footer.tsx | 109 +++++++++++++++++ .../__tests__/CollectionsChips.tests.tsx | 24 ++++ .../__tests__/Footer.tests.tsx | 54 ++++++++ .../Sections/HomeViewSectionCards.tsx | 24 +++- .../Sections/HomeViewSectionCardsChips.tsx | 4 +- .../__tests__/HomeViewSectionCards.tests.tsx | 4 +- src/app/utils/hooks/withSuspense.tsx | 10 +- 10 files changed, 429 insertions(+), 22 deletions(-) create mode 100644 src/app/Scenes/CollectionsByCategory/Body.tsx create mode 100644 src/app/Scenes/CollectionsByCategory/CollectionsChips.tsx create mode 100644 src/app/Scenes/CollectionsByCategory/Footer.tsx create mode 100644 src/app/Scenes/CollectionsByCategory/__tests__/CollectionsChips.tests.tsx create mode 100644 src/app/Scenes/CollectionsByCategory/__tests__/Footer.tests.tsx diff --git a/src/app/Scenes/CollectionsByCategory/Body.tsx b/src/app/Scenes/CollectionsByCategory/Body.tsx new file mode 100644 index 00000000000..7f53f41caa6 --- /dev/null +++ b/src/app/Scenes/CollectionsByCategory/Body.tsx @@ -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 = ({ marketingCollections }) => { + const space = useSpace() + const { params } = useRoute() + const category = params.props.category + + return ( + + {category} + + + Explore collections with {category} + + + + ) +} + +const BodyPlaceholder: React.FC = () => { + const space = useSpace() + + return ( + + + Category + + + Category description text + + + + + + ) +} + +const query = graphql` + query BodyCollectionsByCategoryQuery($category: String!) { + marketingCollections(category: $category, size: 10) { + ...CollectionsChips_marketingCollections + } + } +` + +export const BodyWithSuspense = withSuspense({ + Component: () => { + const { params } = useRoute() + const data = useLazyLoadQuery(query, { + category: params.props.entityID, + }) + + if (!data.marketingCollections) { + return + } + + return + }, + LoadingFallback: BodyPlaceholder, + ErrorFallback: NoFallback, +}) diff --git a/src/app/Scenes/CollectionsByCategory/CollectionsByCategory.tsx b/src/app/Scenes/CollectionsByCategory/CollectionsByCategory.tsx index 63b69703d3d..e9caee30cf8 100644 --- a/src/app/Scenes/CollectionsByCategory/CollectionsByCategory.tsx +++ b/src/app/Scenes/CollectionsByCategory/CollectionsByCategory.tsx @@ -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>() + const { params } = useRoute() + const space = useSpace() - const category = params?.props.category ?? "" + const category = params.props.category return ( - - + + - - {category} + + + + diff --git a/src/app/Scenes/CollectionsByCategory/CollectionsChips.tsx b/src/app/Scenes/CollectionsByCategory/CollectionsChips.tsx new file mode 100644 index 00000000000..127f4ceb7b6 --- /dev/null +++ b/src/app/Scenes/CollectionsByCategory/CollectionsChips.tsx @@ -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 = ({ + 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 ( + + + 1 ? { gap: space(1) } : undefined} + ItemSeparatorComponent={() => } + showsVerticalScrollIndicator={false} + showsHorizontalScrollIndicator={false} + numColumns={numColumns} + data={marketingCollections} + keyExtractor={(item, index) => `item_${index}_${item.internalID}`} + renderItem={({ item }) => ( + + { + if (item?.slug) navigate(`/collection/${item.slug}`) + }} + /> + + )} + /> + + + ) +} + +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 ( + + + } + renderItem={() => ( + + Collection + + )} + /> + + + ) +} + +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] +} diff --git a/src/app/Scenes/CollectionsByCategory/Footer.tsx b/src/app/Scenes/CollectionsByCategory/Footer.tsx new file mode 100644 index 00000000000..cc4cf00bf65 --- /dev/null +++ b/src/app/Scenes/CollectionsByCategory/Footer.tsx @@ -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 = ({ cards, homeViewSectionId }) => { + const { params } = useRoute() + 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 ( + + Explore more categories + + {categories.map((c, index) => ( + handleCategoryPress(c.title, c.entityID)} + > + + {c.title} + + + ))} + + ) +} + +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 ( + + + Explore more categories + + {Array.from({ length: 5 }).map((_, index) => ( + + Category + + ))} + + + ) +} + +const query = graphql` + query FooterCollectionsByCategoryQuery($id: String!) { + homeView { + section(id: $id) { + ...Footer_homeViewSectionCards + } + } + } +` + +export const FooterWithSuspense = withSuspense>({ + Component: ({ homeViewSectionId }) => { + const data = useLazyLoadQuery(query, { + id: homeViewSectionId, + }) + + if (!data?.homeView.section) { + return + } + + return