Skip to content
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

feat: Featured Fairs screen #11476

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 26 additions & 22 deletions src/app/Components/ThreeUpImageLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,61 @@
import { Box } from "@artsy/palette-mobile"
import { Box, Flex } from "@artsy/palette-mobile"
import { CARD_WIDTH } from "app/Components/CardRail/CardRailCard"
import { ImageWithFallback } from "app/Components/ImageWithFallback/ImageWithFallback"
import styled from "styled-components/native"

export const LARGE_IMAGE_SIZE = (CARD_WIDTH / 3) * 2
export const SMALL_IMAGE_SIZE = LARGE_IMAGE_SIZE / 2

interface ThreeUpImageLayoutProps {
imageURLs: string[]
width?: number
}

export const LARGE_IMAGE_SIZE = 180
export const SMALL_IMAGE_SIZE = LARGE_IMAGE_SIZE / 2

export const ThreeUpImageLayout: React.FC<ThreeUpImageLayoutProps> = ({ imageURLs }) => {
export const ThreeUpImageLayout: React.FC<ThreeUpImageLayoutProps> = ({
imageURLs,
width = CARD_WIDTH,
}) => {
// Ensure we have an array of exactly 3 URLs, copying over the last image if we have less than 3
const artworkImageURLs = [null, null, null].reduce((acc: string[], _, i) => {
return [...acc, imageURLs[i] || acc[i - 1]]
}, [])

const largeImageWidth = Math.floor((width * 2) / 3)
const smallImageWidth = Math.floor(largeImageWidth / 2)

return (
<ArtworkImageContainer>
<Flex
flexDirection="row"
justifyContent="space-between"
overflow="hidden"
maxHeight={largeImageWidth}
>
<ImageWithFallback
testID="image-1"
width={LARGE_IMAGE_SIZE}
height={LARGE_IMAGE_SIZE}
width={largeImageWidth}
height={largeImageWidth}
src={artworkImageURLs[0]}
/>
<Division />
<Box>
<ImageWithFallback
testID="image-2"
width={SMALL_IMAGE_SIZE}
height={SMALL_IMAGE_SIZE}
width={smallImageWidth}
height={smallImageWidth}
src={artworkImageURLs[1]}
/>
<Division horizontal />
<ImageWithFallback
testID="image-3"
width={SMALL_IMAGE_SIZE}
height={SMALL_IMAGE_SIZE}
width={smallImageWidth}
height={smallImageWidth}
src={artworkImageURLs[2]}
/>
</Box>
</ArtworkImageContainer>
</Flex>
)
}

export const ArtworkImageContainer = styled.View`
width: 100%;
height: ${LARGE_IMAGE_SIZE}px;
display: flex;
flex-direction: row;
justify-content: space-between;
overflow: hidden;
`

export const Division = styled.View<{ horizontal?: boolean }>`
border: 1px solid white;
${({ horizontal }) => (horizontal ? "height" : "width")}: 1px;
Expand Down
12 changes: 12 additions & 0 deletions src/app/Navigation/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { FairScreen, FairScreenQuery } from "app/Scenes/Fair/Fair"
import { FairAllFollowedArtistsQueryRenderer } from "app/Scenes/Fair/FairAllFollowedArtists"
import { FairArticlesQueryRenderer } from "app/Scenes/Fair/FairArticles"
import { FairMoreInfoQueryRenderer } from "app/Scenes/Fair/FairMoreInfo"
import { FeaturedFairsScreen, featuredFairsScreenQuery } from "app/Scenes/Fair/FeaturedFairsScreen"
import { Favorites } from "app/Scenes/Favorites/Favorites"
import { FeatureQueryRenderer } from "app/Scenes/Feature/Feature"
import { GalleriesForYouScreen } from "app/Scenes/GalleriesForYou/GalleriesForYouScreen"
Expand Down Expand Up @@ -740,6 +741,17 @@ export const artsyDotNetRoutes = defineRoutes([
},
},
},
{
path: "/featured-fairs",
name: "Featured Fairs",
Component: FeaturedFairsScreen,
options: {
screenOptions: {
headerShown: false,
},
},
queries: [featuredFairsScreenQuery],
},
{
path: "/favorites",
name: "Favorites",
Expand Down
67 changes: 67 additions & 0 deletions src/app/Scenes/Fair/FeaturedFairsScreen.tests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { fireEvent, screen, waitForElementToBeRemoved } from "@testing-library/react-native"
import { FeaturedFairsScreen } from "app/Scenes/Fair/FeaturedFairsScreen"
import { navigate } from "app/system/navigation/navigate"
import { setupTestWrapper } from "app/utils/tests/setupTestWrapper"
import { useTracking } from "react-tracking"

describe("FeaturedFairsScreen", () => {
const { renderWithRelay } = setupTestWrapper({
Component: () => <FeaturedFairsScreen />,
})

it("renders fairs", async () => {
renderWithRelay({
Query: () => ({
viewer: {
fairsConnection: mockFairsConnection,
},
}),
})

await waitForElementToBeRemoved(() => screen.queryByTestId("featured-fairs-screen-placeholder"))

expect(screen.getByText("Art Saloon International")).toBeTruthy()
expect(screen.getByText("Art Saloon II")).toBeTruthy()
})

it("opens fair screen and tracks on press", async () => {
renderWithRelay({
Query: () => ({
viewer: {
fairsConnection: mockFairsConnection,
},
}),
})

await waitForElementToBeRemoved(() => screen.queryByTestId("featured-fairs-screen-placeholder"))

fireEvent.press(screen.getByText("Art Saloon International"))

expect(useTracking().trackEvent).toHaveBeenCalledWith({
action: "tappedFairGroup",
context_screen_owner_type: "fairs",
destination_screen_owner_id: "art-saloon-international",
destination_screen_owner_slug: '<mock-value-for-field-"slug">',
destination_screen_owner_type: "fair",
})

expect(navigate).toHaveBeenCalledWith('/fair/<mock-value-for-field-"slug">')
})
})

const mockFairsConnection = {
edges: [
{
node: {
internalID: "art-saloon-international",
name: "Art Saloon International",
},
},
{
node: {
internalID: "art-saloon-ii",
name: "Art Saloon II",
},
},
],
}
185 changes: 185 additions & 0 deletions src/app/Scenes/Fair/FeaturedFairsScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { ActionType, OwnerType } from "@artsy/cohesion"
import { Box, Screen, Spacer, useScreenDimensions, useSpace } from "@artsy/palette-mobile"
import { FairCard_fair$data } from "__generated__/FairCard_fair.graphql"
import { FeaturedFairsScreenQuery } from "__generated__/FeaturedFairsScreenQuery.graphql"
import { FeaturedFairsScreen_viewer$key } from "__generated__/FeaturedFairsScreen_viewer.graphql"
import { PAGE_SIZE } from "app/Components/constants"
import { useNumColumns } from "app/Scenes/Articles/ArticlesList"
import { FairCard } from "app/Scenes/HomeView/Sections/FairCard"
import { HORIZONTAL_FLATLIST_INTIAL_NUMBER_TO_RENDER_DEFAULT } from "app/Scenes/HomeView/helpers/constants"
import { extractNodes } from "app/utils/extractNodes"
import { NoFallback, withSuspense } from "app/utils/hooks/withSuspense"
import { AnimatedMasonryListFooter } from "app/utils/masonryHelpers/AnimatedMasonryListFooter"
import {
PlaceholderBox,
ProvidePlaceholderContext,
RandomWidthPlaceholderText,
} from "app/utils/placeholders"
import { useRefreshControl } from "app/utils/refreshHelpers"
import { ProvideScreenTrackingWithCohesionSchema } from "app/utils/track"
import { screen } from "app/utils/track/helpers"
import { times } from "lodash"
import { useCallback } from "react"
import { FlatList } from "react-native"
import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay"
import { useTracking } from "react-tracking"

interface FeaturedFairsProps {
viewer: FeaturedFairsScreen_viewer$key
}

export const FeaturedFairs: React.FC<FeaturedFairsProps> = ({ viewer }) => {
const { data, loadNext, hasNext, isLoadingNext, refetch } = usePaginationFragment(
viewerFragment,
viewer
)

const RefreshControl = useRefreshControl(refetch)
const { trackEvent } = useTracking()
const { width: screenWidth } = useScreenDimensions()
const space = useSpace()
const numColumns = useNumColumns()

const onEndReached = useCallback(() => {
if (!!hasNext && !isLoadingNext) {
loadNext?.(PAGE_SIZE)
}
}, [hasNext, isLoadingNext])

const handleOnPress = (fair: FairCard_fair$data) => {
trackEvent(tracks.tapFair(fair.internalID, fair.slug || ""))
}

const fairs = extractNodes(data?.fairsConnection)

if (!fairs?.length) return null

return (
<ProvideScreenTrackingWithCohesionSchema
info={screen({
context_screen_owner_type: OwnerType.featuredFairs,
})}
>
<Screen>
<Screen.AnimatedHeader title="Featured Fairs" />
<Screen.StickySubHeader title="Featured Fairs" />

<Screen.Body>
<Screen.ScrollView>
<Spacer y={2} />
<Screen.FlatList
data={fairs}
onEndReached={onEndReached}
refreshControl={RefreshControl}
initialNumToRender={HORIZONTAL_FLATLIST_INTIAL_NUMBER_TO_RENDER_DEFAULT}
keyExtractor={(item) => item.internalID}
numColumns={numColumns}
renderItem={({ item }) => {
return (
<FairCard fair={item} width={screenWidth - space(4)} onPress={handleOnPress} />
)
}}
ItemSeparatorComponent={() => <Spacer y={2} />}
ListFooterComponent={
hasNext ? (
<AnimatedMasonryListFooter
shouldDisplaySpinner={!!fairs.length && !!isLoadingNext && !!hasNext}
/>
) : null
}
/>
</Screen.ScrollView>
</Screen.Body>
</Screen>
</ProvideScreenTrackingWithCohesionSchema>
)
}

const viewerFragment = graphql`
fragment FeaturedFairsScreen_viewer on Viewer
@refetchable(queryName: "FeaturedFairsScreen_viewerRefetch")
@argumentDefinitions(count: { type: "Int", defaultValue: 10 }, cursor: { type: "String" }) {
fairsConnection(
after: $cursor
first: $count
status: CURRENT
sort: START_AT_DESC
hasFullFeature: true
) @connection(key: "FeaturedFairsScreen_fairsConnection") {
edges {
node {
internalID
...FairCard_fair
}
}
}
}
`

// TODO: Update query to use new featuredFairsConnection
export const featuredFairsScreenQuery = graphql`
query FeaturedFairsScreenQuery {
viewer {
...FeaturedFairsScreen_viewer
}
}
`

export const FeaturedFairsScreen: React.FC = withSuspense({
Component: () => {
const data = useLazyLoadQuery<FeaturedFairsScreenQuery>(featuredFairsScreenQuery, {})

if (!data?.viewer) {
return null
}

return <FeaturedFairs viewer={data.viewer} />
},
LoadingFallback: () => <FeaturedFairsScreenPlaceholder />,
ErrorFallback: NoFallback,
})

export const FeaturedFairsScreenPlaceholder: React.FC = () => {
const numColumns = useNumColumns()

return (
<Screen testID="featured-fairs-screen-placeholder">
<Screen.AnimatedHeader title="Featured Fairs" />
<Screen.StickySubHeader title="Featured Fairs" />

<Screen.Body fullwidth>
<ProvidePlaceholderContext>
<Spacer y={2} />
<FlatList
numColumns={numColumns}
data={times(6)}
keyExtractor={(item) => `${item}-${numColumns}`}
renderItem={({ index }) => {
return (
<Box key={index} mx={2}>
<PlaceholderBox aspectRatio={1.5} width="100%" marginBottom={10} />
<Spacer y={1} />
<RandomWidthPlaceholderText minWidth={200} maxWidth={250} height={16} />
<RandomWidthPlaceholderText minWidth={200} maxWidth={250} height={16} />
</Box>
)
}}
ItemSeparatorComponent={() => <Spacer y={2} />}
/>
</ProvidePlaceholderContext>
</Screen.Body>
</Screen>
)
}

export const tracks = {
tapFair: (fairID: string, fairSlug: string) => ({
action: ActionType.tappedFairGroup,
// TODO: Update context module
// context_module: ContextModule.featuredFairs,
context_screen_owner_type: OwnerType.fairs,
destination_screen_owner_type: OwnerType.fair,
destination_screen_owner_id: fairID,
destination_screen_owner_slug: fairSlug,
}),
}
Loading