Skip to content

Commit

Permalink
feat: Featured Fairs screen
Browse files Browse the repository at this point in the history
  • Loading branch information
olerichter00 committed Feb 10, 2025
1 parent d2b22f1 commit d093ac7
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 39 deletions.
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 @@ -678,6 +679,17 @@ export const artsyDotNetRoutes = defineRoutes([
},
},
},
{
path: "/fairs/featured",
name: "Featured Fairs",
Component: FeaturedFairsScreen,
options: {
screenOptions: {
headerShown: false,
},
},
queries: [featuredFairsScreenQuery],
},
{
path: "/fair/:fairID",
name: "Fair",
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({
// TODO: Update owner type
context_screen_owner_type: OwnerType.articles,
})}
>
<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
}
}
}
}
`

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

0 comments on commit d093ac7

Please sign in to comment.