diff --git a/src/features/offer/components/MoviesScreeningCalendar/MovieCalendarContext.tsx b/src/features/offer/components/MoviesScreeningCalendar/MovieCalendarContext.tsx new file mode 100644 index 00000000000..0cdd30a697d --- /dev/null +++ b/src/features/offer/components/MoviesScreeningCalendar/MovieCalendarContext.tsx @@ -0,0 +1,149 @@ +import React, { + createContext, + useCallback, + useContext, + useMemo, + useRef, + useEffect, + useState, + PropsWithChildren, +} from 'react' +import { Animated, View, ViewStyle } from 'react-native' +import { FlatList } from 'react-native-gesture-handler' +import { Easing } from 'react-native-reanimated' +import styled from 'styled-components/native' + +import { MovieCalendar } from 'features/offer/components/MovieCalendar/MovieCalendar' +import { handleMovieCalendarScroll } from 'features/offer/components/MoviesScreeningCalendar/utils' +import { useNextDays } from 'features/offer/helpers/useNextDays/useNextDays' +import { Anchor } from 'ui/components/anchor/Anchor' +import { useScrollToAnchor } from 'ui/components/anchor/AnchorContext' +import { useLayout } from 'ui/hooks/useLayout' + +type MovieCalendarContextType = { + selectedDate: Date + goToDate: (date: Date) => void +} + +const MovieCalendarContext = createContext(undefined) + +const AnimatedCalendarView: React.FC> = ({ + selectedDate, + children, +}) => { + const fadeAnim = useRef(new Animated.Value(0)).current + const translateAnim = useRef(new Animated.Value(0)).current + const [width, setWidth] = useState(0) + + useEffect(() => { + translateAnim.setValue(0) + fadeAnim.setValue(0) + Animated.timing(translateAnim, { + toValue: 1, + duration: 200, + useNativeDriver: true, + easing: Easing.out(Easing.ease), + }).start() + Animated.timing(fadeAnim, { + toValue: 1, + duration: 300, + easing: Easing.in(Easing.ease), + useNativeDriver: true, + }).start() + }, [fadeAnim, translateAnim, selectedDate]) + + return ( + + { + setWidth(nativeEvent.layout.width) + }} + style={{ + opacity: fadeAnim, + transform: [ + { translateX: Animated.subtract(Animated.multiply(translateAnim, width), width) }, + ], + }}> + {children} + + + ) +} + +export const MovieCalendarProvider: React.FC<{ + nbOfDays: number + children: React.ReactNode + containerStyle?: ViewStyle +}> = ({ nbOfDays, containerStyle, children }) => { + const { dates, selectedDate, setSelectedDate } = useNextDays(nbOfDays) + const flatListRef = useRef(null) + const { width: flatListWidth, onLayout: onFlatListLayout } = useLayout() + const { width: itemWidth, onLayout: onItemLayout } = useLayout() + const scrollToAnchor = useScrollToAnchor() + + const scrollToMiddleElement = useCallback( + (currentIndex: number) => { + const { offset } = handleMovieCalendarScroll(currentIndex, flatListWidth, itemWidth) + + flatListRef.current?.scrollToOffset({ + animated: true, + offset, + }) + }, + [flatListRef, flatListWidth, itemWidth] + ) + + useEffect(() => { + const currentIndex = dates.findIndex( + (date) => (date as Date).toDateString() === selectedDate.toDateString() + ) + + scrollToMiddleElement(currentIndex) + }, [selectedDate, dates, scrollToMiddleElement]) + + useEffect(() => { + if (flatListRef?.current) { + flatListRef.current?.scrollToOffset({ offset: 0 }) + } + }, [flatListRef]) + + const goToDate = useCallback( + (date: Date) => { + scrollToAnchor('movie-calendar') + setSelectedDate(date) + }, + [scrollToAnchor, setSelectedDate] + ) + + const value = useMemo(() => ({ selectedDate, goToDate }), [selectedDate, goToDate]) + + return ( + + + + + + + {children} + + ) +} + +const AnimationContainer = styled.View({ overflow: 'hidden' }) + +export const useMovieCalendar = (): MovieCalendarContextType => { + const context = useContext(MovieCalendarContext) + if (context === undefined) { + throw new Error('useMovieCalendar must be used within a MovieCalendarProvider') + } + return context +} diff --git a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx index cb4baa08dc9..c83a95228e4 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx +++ b/src/features/offer/components/MoviesScreeningCalendar/MovieOfferTile.tsx @@ -1,5 +1,5 @@ -import React, { FC, useCallback, useMemo } from 'react' -import { FlatList, View } from 'react-native' +import React, { FC, useMemo } from 'react' +import { View } from 'react-native' import styled from 'styled-components/native' import type { OfferPreviewResponse } from 'api/gen' @@ -9,13 +9,12 @@ import { } from 'features/offer/components/MovieScreeningCalendar/useMovieScreeningCalendar' import { useSelectedDateScreening } from 'features/offer/components/MovieScreeningCalendar/useSelectedDateScreenings' import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' +import { useMovieCalendar } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' import { NextScreeningButton } from 'features/offer/components/MoviesScreeningCalendar/NextScreeningButton' -import { handleMovieCalendarScroll } from 'features/offer/components/MoviesScreeningCalendar/utils' import { useOfferCTAButton } from 'features/offer/components/OfferCTAButton/useOfferCTAButton' import { formatDuration } from 'features/offer/helpers/formatDuration/formatDuration' import { VenueOffers } from 'features/venue/api/useVenueOffers' import { useSubcategoriesMapping } from 'libs/subcategories' -import { useScrollToAnchor } from 'ui/components/anchor/AnchorContext' import { EventCardList } from 'ui/components/eventCard/EventCardList' import { HorizontalOfferTile } from 'ui/components/tiles/HorizontalOfferTile' import { Spacer } from 'ui/theme' @@ -23,34 +22,22 @@ import { Spacer } from 'ui/theme' type MovieOfferTileProps = { movieOffer: MovieOffer venueOffers: VenueOffers - date: Date isLast: boolean nextScreeningDate?: Date - setSelectedDate: (date: Date) => void - nextDateIndex: number - flatListRef: React.MutableRefObject - flatListWidth: number - itemWidth: number } export const MovieOfferTile: FC = ({ venueOffers, - date, movieOffer: { offer }, isLast, nextScreeningDate, - setSelectedDate, - nextDateIndex, - flatListRef, - flatListWidth, - itemWidth, }) => { const movieScreenings = getMovieScreenings(offer.stocks) - const scrollToAnchor = useScrollToAnchor() + const { goToDate, selectedDate } = useMovieCalendar() const selectedScreeningStock = useMemo( - () => movieScreenings[getDateString(String(date))], - [movieScreenings, date] + () => movieScreenings[getDateString(String(selectedDate))], + [movieScreenings, selectedDate] ) const subcategoriesMapping = useSubcategoriesMapping() @@ -60,18 +47,6 @@ export const MovieOfferTile: FC = ({ offer.isExternalBookingsDisabled ) - const scrollToMiddleElement = useCallback( - (currentIndex: number) => { - const { offset } = handleMovieCalendarScroll(currentIndex, flatListWidth, itemWidth) - - flatListRef.current?.scrollToOffset({ - animated: true, - offset, - }) - }, - [flatListRef, flatListWidth, itemWidth] - ) - const { onPress: onPressOfferCTA, CTAOfferModal, @@ -106,11 +81,7 @@ export const MovieOfferTile: FC = ({ { - scrollToAnchor('venue-calendar') - setSelectedDate(nextScreeningDate) - scrollToMiddleElement(nextDateIndex) - }} + onPress={() => goToDate(nextScreeningDate)} /> ) : ( diff --git a/src/features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar.tsx b/src/features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar.tsx index 04799cd991b..901ff10bc27 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar.tsx +++ b/src/features/offer/components/MoviesScreeningCalendar/MoviesScreeningCalendar.tsx @@ -1,20 +1,12 @@ import { useRoute } from '@react-navigation/native' -import React, { FunctionComponent, useMemo, useRef, useState, useCallback, useEffect } from 'react' -import { FlatList, Animated, Easing, View } from 'react-native' +import React, { FunctionComponent } from 'react' import styled from 'styled-components/native' import { SubcategoryIdEnum } from 'api/gen' import { UseRouteType } from 'features/navigation/RootNavigator/types' -import { useOffersStocks } from 'features/offer/api/useOffersStocks' -import { MovieCalendar } from 'features/offer/components/MovieCalendar/MovieCalendar' -import { filterOffersStocksByDate } from 'features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate' -import { - getNextMoviesByDate, - MovieOffer, -} from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' -import { MovieOfferTile } from 'features/offer/components/MoviesScreeningCalendar/MovieOfferTile' +import { MovieCalendarProvider } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { VenueCalendar } from 'features/offer/components/MoviesScreeningCalendar/VenueCalendar' import { OfferTile } from 'features/offer/components/OfferTile/OfferTile' -import { useNextDays } from 'features/offer/helpers/useNextDays/useNextDays' import { VenueOffers } from 'features/venue/api/useVenueOffers' import { useFeatureFlag } from 'libs/firebase/firestore/featureFlags/useFeatureFlag' import { RemoteStoreFeatureFlags } from 'libs/firebase/firestore/types' @@ -22,11 +14,9 @@ import { formatDates } from 'libs/parsers/formatDates' import { getDisplayPrice } from 'libs/parsers/getDisplayPrice' import { useCategoryHomeLabelMapping, useCategoryIdMapping } from 'libs/subcategories' import { Offer } from 'shared/offer/types' -import { Anchor } from 'ui/components/anchor/Anchor' import { PassPlaylist } from 'ui/components/PassPlaylist' import { CustomListRenderItem } from 'ui/components/Playlist' import { SectionWithDivider } from 'ui/components/SectionWithDivider' -import { useLayout } from 'ui/hooks/useLayout' import { LENGTH_M, RATIO_HOME_IMAGE, Spacer, TypoDS } from 'ui/theme' import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' @@ -36,86 +26,15 @@ type Props = { const keyExtractor = (item: Offer) => item.objectID -const useMoviesScreeningsList = (offerIds: number[]) => { - const { selectedDate, setSelectedDate, dates } = useNextDays(15) - const { data: offersWithStocks } = useOffersStocks({ offerIds }) - - const moviesOffers: MovieOffer[] = useMemo(() => { - const filteredOffersWithStocks = filterOffersStocksByDate( - offersWithStocks?.offers || [], - selectedDate - ) - const nextScreeningOffers = getNextMoviesByDate(offersWithStocks?.offers || [], selectedDate) - - return [...filteredOffersWithStocks, ...nextScreeningOffers] - }, [offersWithStocks?.offers, selectedDate]).filter( - (offer) => offer.offer.subcategoryId === SubcategoryIdEnum.SEANCE_CINE - ) - - return { - dates, - selectedDate, - setSelectedDate, - moviesOffers, - } -} - export const MoviesScreeningCalendar: FunctionComponent = ({ venueOffers }) => { - const { width: flatListWidth, onLayout: onFlatListLayout } = useLayout() - const { width: itemWidth, onLayout: onItemLayout } = useLayout() const { params: routeParams } = useRoute>() const isNewOfferTileDisplayed = useFeatureFlag(RemoteStoreFeatureFlags.WIP_NEW_OFFER_TILE) const offerIds = venueOffers.hits.map((offer) => Number(offer.objectID)) - const flatListRef = useRef(null) - const fadeAnim = useRef(new Animated.Value(0)).current - const translateAnim = useRef(new Animated.Value(0)).current - const [width, setWidth] = useState(0) - const { - dates: nextFifteenDates, - selectedDate, - setSelectedDate, - moviesOffers, - } = useMoviesScreeningsList(offerIds) const nonScreeningOffers = venueOffers.hits.filter( (offer) => offer.offer.subcategoryId !== SubcategoryIdEnum.SEANCE_CINE ) - useEffect(() => { - translateAnim.setValue(0) - fadeAnim.setValue(0) - Animated.timing(translateAnim, { - toValue: 1, - duration: 200, - useNativeDriver: true, - easing: Easing.out(Easing.ease), - }).start() - Animated.timing(fadeAnim, { - toValue: 1, - duration: 300, - easing: Easing.in(Easing.ease), - useNativeDriver: true, - }).start() - }, [fadeAnim, translateAnim, selectedDate]) - - const getIsLast = useCallback( - (index: number) => { - const length = moviesOffers.length ?? 0 - - return index === length - 1 - }, - [moviesOffers.length] - ) - - const getNextDateIndex = useCallback( - (nextDate: Date) => { - return nextFifteenDates.findIndex( - (date) => date.toISOString().split('T')[0] === nextDate.toISOString().split('T')[0] - ) - }, - [nextFifteenDates] - ) - const mapping = useCategoryIdMapping() const labelMapping = useCategoryHomeLabelMapping() @@ -145,47 +64,10 @@ export const MoviesScreeningCalendar: FunctionComponent = ({ venueOffers return ( - - - - - { - setWidth(nativeEvent.layout.width) - }} - style={{ - opacity: fadeAnim, - transform: [ - { translateX: Animated.subtract(Animated.multiply(translateAnim, width), width) }, - ], - }}> - {moviesOffers.map((movie, index) => ( - - ))} - - + + + {nonScreeningOffers.length > 0 ? ( = ({ venueOffers ) } -const Container = styled(View)(({ theme }) => ({ - marginHorizontal: theme.contentPage.marginHorizontal, -})) - const PlaylistTitleText = styled(TypoDS.Title3).attrs(getHeadingAttrs(2))`` diff --git a/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx b/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx new file mode 100644 index 00000000000..83412af6cf7 --- /dev/null +++ b/src/features/offer/components/MoviesScreeningCalendar/VenueCalendar.tsx @@ -0,0 +1,72 @@ +import React, { FunctionComponent, useMemo, useCallback } from 'react' +import { View } from 'react-native' +import styled from 'styled-components/native' + +import { SubcategoryIdEnum } from 'api/gen' +import { useOffersStocks } from 'features/offer/api/useOffersStocks' +import { filterOffersStocksByDate } from 'features/offer/components/MoviesScreeningCalendar/filterOffersStocksByDate' +import { + getNextMoviesByDate, + MovieOffer, +} from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' +import { useMovieCalendar } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { MovieOfferTile } from 'features/offer/components/MoviesScreeningCalendar/MovieOfferTile' +import { VenueOffers } from 'features/venue/api/useVenueOffers' +import { Spacer } from 'ui/theme' + +type Props = { + venueOffers: VenueOffers + offerIds: number[] +} + +const useMoviesScreeningsList = (offerIds: number[]) => { + const { selectedDate } = useMovieCalendar() + const { data: offersWithStocks } = useOffersStocks({ offerIds }) + + const moviesOffers: MovieOffer[] = useMemo(() => { + const filteredOffersWithStocks = filterOffersStocksByDate( + offersWithStocks?.offers || [], + selectedDate + ) + const nextScreeningOffers = getNextMoviesByDate(offersWithStocks?.offers || [], selectedDate) + + return [...filteredOffersWithStocks, ...nextScreeningOffers] + }, [offersWithStocks?.offers, selectedDate]).filter( + (offer) => offer.offer.subcategoryId === SubcategoryIdEnum.SEANCE_CINE + ) + + return { + moviesOffers, + } +} + +export const VenueCalendar: FunctionComponent = ({ venueOffers, offerIds }) => { + const { moviesOffers } = useMoviesScreeningsList(offerIds) + + const getIsLast = useCallback( + (index: number) => { + const length = moviesOffers.length ?? 0 + return index === length - 1 + }, + [moviesOffers.length] + ) + + return ( + + + {moviesOffers.map((movie, index) => ( + + ))} + + ) +} + +const Container = styled(View)(({ theme }) => ({ + marginHorizontal: theme.contentPage.marginHorizontal, +})) diff --git a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts index 013325c1e3a..9591fc7f6a5 100644 --- a/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts +++ b/src/features/offer/components/MoviesScreeningCalendar/moviesOffer.builder.ts @@ -1,4 +1,11 @@ -import { addDays, differenceInMilliseconds, isAfter, isBefore, isSameDay } from 'date-fns' +import { + addDays, + differenceInMilliseconds, + isAfter, + isBefore, + isSameDay, + startOfDay, +} from 'date-fns' import { OfferResponseV2, OfferStockResponse } from 'api/gen' import { MovieOffer } from 'features/offer/components/MoviesScreeningCalendar/getNextMoviesByDate' @@ -154,7 +161,7 @@ export const moviesOfferBuilder = (offersWithStocks: OfferResponseV2[] = []) => } const isDateNotWithinNext15Days = (referenceDate: Date, targetDate: Date) => { - const datePlus15Days = addDays(referenceDate, 15) + const datePlus15Days = addDays(startOfDay(referenceDate), 15) return isAfter(targetDate, datePlus15Days) } diff --git a/src/features/offer/components/OfferContent/OfferContentBase.tsx b/src/features/offer/components/OfferContent/OfferContentBase.tsx index ce724d2b4d8..13a0db6f8c2 100644 --- a/src/features/offer/components/OfferContent/OfferContentBase.tsx +++ b/src/features/offer/components/OfferContent/OfferContentBase.tsx @@ -1,5 +1,18 @@ -import React, { FunctionComponent, ReactElement, useCallback, useEffect, useMemo } from 'react' -import { NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native' +import React, { + FunctionComponent, + ReactElement, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react' +import { + NativeScrollEvent, + NativeSyntheticEvent, + StyleProp, + ViewStyle, + ScrollView, +} from 'react-native' import { IOScrollView as IntersectionObserverScrollView } from 'react-native-intersection-observer' import styled from 'styled-components/native' @@ -15,6 +28,7 @@ import { OfferContentProps } from 'features/offer/types' import { analytics, isCloseToBottom } from 'libs/analytics' import { useFunctionOnce } from 'libs/hooks' import { useOpacityTransition } from 'ui/animations/helpers/useOpacityTransition' +import { AnchorProvider } from 'ui/components/anchor/AnchorContext' type OfferContentBaseProps = OfferContentProps & { BodyWrapper: FunctionComponent @@ -40,6 +54,8 @@ export const OfferContentBase: FunctionComponent = ({ otherCategoriesSimilarOffers, apiRecoParamsOtherCategories, } = useOfferPlaylist({ offer, offerSearchGroup: subcategory.searchGroupName, searchGroupList }) + const scrollViewRef = useRef(null) + const scrollYRef = useRef(0) const logConsultWholeOffer = useFunctionOnce(() => { analytics.logConsultWholeOffer(offer.id) @@ -82,37 +98,52 @@ export const OfferContentBase: FunctionComponent = ({ listener: scrollEventListener, }) + const handleCheckScrollY = () => { + return scrollYRef.current + } + + const handleScroll = useCallback( + (event: NativeSyntheticEvent) => { + onScroll(event) + scrollYRef.current = event.nativeEvent.contentOffset.y + }, + [onScroll] + ) + return ( - - - 0 ? onOfferPreviewPress : undefined} - /> - + + + 0 ? onOfferPreviewPress : undefined} + /> + + + - - - + + {footer} ) diff --git a/src/features/offer/components/OfferNewXPCine/CineBlock.tsx b/src/features/offer/components/OfferNewXPCine/CineBlock.tsx index 6bdd86fb074..5e46e0dfeb2 100644 --- a/src/features/offer/components/OfferNewXPCine/CineBlock.tsx +++ b/src/features/offer/components/OfferNewXPCine/CineBlock.tsx @@ -3,22 +3,29 @@ import { View } from 'react-native' import styled from 'styled-components/native' import { OfferResponseV2 } from 'api/gen' +import { useMovieCalendar } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { NextScreeningButton } from 'features/offer/components/MoviesScreeningCalendar/NextScreeningButton' import { OfferEventCardList } from 'features/offer/components/OfferEventCardList/OfferEventCardList' import { VenueBlock } from 'features/offer/components/OfferVenueBlock/VenueBlock' import { Spacer } from 'ui/theme' type Props = { offer: OfferResponseV2 - selectedDate: Date onSeeVenuePress?: VoidFunction + nextDate?: Date } -export const CineBlock: FunctionComponent = ({ offer, onSeeVenuePress, selectedDate }) => { +export const CineBlock: FunctionComponent = ({ offer, onSeeVenuePress, nextDate }) => { + const { selectedDate, goToDate } = useMovieCalendar() return ( - + {nextDate ? ( + goToDate(nextDate)} /> + ) : ( + + )} ) } diff --git a/src/features/offer/components/OfferNewXPCine/OfferNewXPCineBlock.tsx b/src/features/offer/components/OfferNewXPCine/OfferNewXPCineBlock.tsx index 27390bfb2ce..343e4a85dcf 100644 --- a/src/features/offer/components/OfferNewXPCine/OfferNewXPCineBlock.tsx +++ b/src/features/offer/components/OfferNewXPCine/OfferNewXPCineBlock.tsx @@ -1,21 +1,14 @@ -import React, { FC, useEffect, useRef, useCallback, useState } from 'react' -import { FlatList, View } from 'react-native' -import Animated, { useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated' +import React, { FC } from 'react' +import { View, ViewStyle } from 'react-native' import styled, { useTheme } from 'styled-components/native' import { OfferResponseV2 } from 'api/gen' -import { MovieCalendar } from 'features/offer/components/MovieCalendar/MovieCalendar' -import { CineBlock } from 'features/offer/components/OfferNewXPCine/CineBlock' -import { CineBlockSkeleton } from 'features/offer/components/OfferNewXPCine/CineBlockSkeleton' -import { useGetVenuesByDay } from 'features/offer/helpers/useGetVenueByDay/useGetVenuesByDay' -import { useNextDays } from 'features/offer/helpers/useNextDays/useNextDays' -import { ButtonSecondary } from 'ui/components/buttons/ButtonSecondary' -import { PlainMore } from 'ui/svg/icons/PlainMore' +import { MovieCalendarProvider } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { OfferNewXPCineContent } from 'features/offer/components/OfferNewXPCine/OfferNewXPCineContent' +import { AppThemeType } from 'theme' import { getSpacing, Spacer, TypoDS } from 'ui/theme' import { getHeadingAttrs } from 'ui/theme/typographyAttrs/getHeadingAttrs' -const ANIMATION_DURATION = 300 - type Props = { title: string offer: OfferResponseV2 @@ -24,124 +17,28 @@ type Props = { export const OfferNewXPCineBlock: FC = ({ title, onSeeVenuePress, offer }) => { const theme = useTheme() - const flatListRef = useRef(null) - const { selectedDate, setSelectedDate, dates } = useNextDays(15) - const { - increaseCount, - isEnd: hasReachedVenueListEnd, - items, - isLoading, - } = useGetVenuesByDay(selectedDate, offer, { initialCount: 6, nextCount: 3, radiusKm: 50 }) - - const { animatedStyle, onContentSizeChange } = useAnimatedHeight() - - useEffect(() => { - if (flatListRef?.current) { - flatListRef.current?.scrollToOffset({ offset: 0 }) - } - }, [flatListRef]) return ( {title} - - - - - - - - {isLoading ? : null} - ( - - - - - - )} - /> - {hasReachedVenueListEnd ? null : ( - - - Aucune séance ne te correspond ? - - - - )} - + + + ) } -const useAnimatedHeight = () => { - const [contentHeight, setContentHeight] = useState(0) - const isFirstRender = useRef(true) - - const animatedStyle = useAnimatedStyle(() => { - if (isFirstRender.current) { - isFirstRender.current = false - return { height: contentHeight } - } - - return { - height: withTiming(contentHeight, { - duration: ANIMATION_DURATION, - easing: Easing.bezier(0.25, 0.1, 0.25, 1), - }), - } - }, [contentHeight]) - - const onContentSizeChange = useCallback((_width: number, height: number) => { - setContentHeight(height) - }, []) - - return { animatedStyle, onContentSizeChange } -} +const getCalendarStyle = (theme: AppThemeType): ViewStyle => ({ + marginRight: theme.isDesktopViewport ? -getSpacing(16) : 0, +}) const Container = styled(View)({ marginVertical: 0, }) -const MovieCalendarContainer = styled(View)(({ theme }) => ({ - marginRight: theme.isDesktopViewport ? -getSpacing(16) : 0, // cancels padding of the parent container -})) - -const TitleContainer = styled(View)(({ theme }) => ({ - marginHorizontal: theme.isDesktopViewport ? undefined : theme.contentPage.marginHorizontal, -})) - -const Divider = styled.View(({ theme }) => ({ - height: 1, - backgroundColor: theme.colors.greyMedium, +const TitleContainer = styled.View(({ theme }) => ({ marginHorizontal: theme.isDesktopViewport ? undefined : theme.contentPage.marginHorizontal, })) - -const SeeMoreContainer = styled.View(({ theme }) => ({ - alignItems: theme.isMobileViewport ? 'center' : undefined, -})) - -const Text = styled(TypoDS.Body)(({ theme }) => ({ - color: theme.colors.greyDark, -})) diff --git a/src/features/offer/components/OfferNewXPCine/OfferNewXPCineContent.tsx b/src/features/offer/components/OfferNewXPCine/OfferNewXPCineContent.tsx new file mode 100644 index 00000000000..3b6f1d3990d --- /dev/null +++ b/src/features/offer/components/OfferNewXPCine/OfferNewXPCineContent.tsx @@ -0,0 +1,105 @@ +import React, { FC, useRef, useCallback, useState } from 'react' +import { View } from 'react-native' +import Animated, { useAnimatedStyle, withTiming, Easing } from 'react-native-reanimated' +import styled, { useTheme } from 'styled-components/native' + +import { OfferResponseV2 } from 'api/gen' +import { useMovieCalendar } from 'features/offer/components/MoviesScreeningCalendar/MovieCalendarContext' +import { CineBlock } from 'features/offer/components/OfferNewXPCine/CineBlock' +import { CineBlockSkeleton } from 'features/offer/components/OfferNewXPCine/CineBlockSkeleton' +import { useGetVenuesByDay } from 'features/offer/helpers/useGetVenueByDay/useGetVenuesByDay' +import { ButtonSecondary } from 'ui/components/buttons/ButtonSecondary' +import { PlainMore } from 'ui/svg/icons/PlainMore' +import { Spacer, TypoDS } from 'ui/theme' + +const ANIMATION_DURATION = 300 + +export const OfferNewXPCineContent: FC<{ + offer: OfferResponseV2 + onSeeVenuePress?: VoidFunction +}> = ({ offer, onSeeVenuePress }) => { + const theme = useTheme() + const { animatedStyle, onContentSizeChange } = useAnimatedHeight() + const { selectedDate } = useMovieCalendar() + const { + increaseCount, + isEnd: hasReachedVenueListEnd, + items, + isLoading, + } = useGetVenuesByDay(selectedDate, offer, { initialCount: 6, nextCount: 3, radiusKm: 50 }) + + return ( + + {isLoading ? : null} + ( + + + + + + )} + /> + {hasReachedVenueListEnd ? null : ( + + + Aucune séance ne te correspond ? + + + + )} + + ) +} + +const useAnimatedHeight = () => { + const [contentHeight, setContentHeight] = useState(0) + const isFirstRender = useRef(true) + + const animatedStyle = useAnimatedStyle(() => { + if (isFirstRender.current) { + isFirstRender.current = false + return { height: contentHeight } + } + + return { + height: withTiming(contentHeight, { + duration: ANIMATION_DURATION, + easing: Easing.bezier(0.25, 0.1, 0.25, 1), + }), + } + }, [contentHeight]) + + const onContentSizeChange = useCallback((_width: number, height: number) => { + setContentHeight(height) + }, []) + + return { animatedStyle, onContentSizeChange } +} + +const Divider = styled.View(({ theme }) => ({ + height: 1, + backgroundColor: theme.colors.greyMedium, + marginHorizontal: theme.isDesktopViewport ? undefined : theme.contentPage.marginHorizontal, +})) + +const SeeMoreContainer = styled.View(({ theme }) => ({ + alignItems: theme.isMobileViewport ? 'center' : undefined, +})) + +const Text = styled(TypoDS.Body)(({ theme }) => ({ + color: theme.colors.greyDark, +})) diff --git a/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts b/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts index 62244cd4391..6a17a6ddf90 100644 --- a/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts +++ b/src/features/offer/helpers/useGetVenueByDay/useGetVenuesByDay.ts @@ -7,7 +7,6 @@ import { useOffersStocks } from 'features/offer/api/useOffersStocks' import { moviesOfferBuilder } from 'features/offer/components/MoviesScreeningCalendar/moviesOffer.builder' import { useIsUserUnderage } from 'features/profile/helpers/useIsUserUnderage' import { initialSearchState } from 'features/search/context/reducer' -import { DATE_FILTER_OPTIONS } from 'features/search/enums' import { useLocation } from 'libs/location' import { LocationMode } from 'libs/location/types' import { Offer } from 'shared/offer/types' @@ -39,7 +38,6 @@ export const useGetVenuesByDay = (date: Date, offer: OfferResponseV2, options?: parameters: { ...initialSearchState, allocineId: offer.extraData?.allocineId ?? undefined, - date: { selectedDate: date.toISOString(), option: DATE_FILTER_OPTIONS.USER_PICK }, distinct: false, }, buildLocationParameterParams: { @@ -58,32 +56,45 @@ export const useGetVenuesByDay = (date: Date, offer: OfferResponseV2, options?: setCount(initialCount) }, [date, initialCount]) - const filteredOffers = useMemo( + const dayOffers = useMemo( () => moviesOfferBuilder(offersWithStocks?.offers) + .withoutMoviesAfter15Days() .withMoviesOnDay(date) - .sortedByDistance(location) - .buildOfferResponse(), - [date, location, offersWithStocks?.offers] + .build(), + [date, offersWithStocks?.offers] ) - const displayOffers = filteredOffers.slice(0, count) + const nextOffers = useMemo(() => { + return moviesOfferBuilder(offersWithStocks?.offers) + .withoutMoviesAfter15Days() + .withoutMoviesOnDay(date) + .withNextScreeningFromDate(date) + .build() + }, [date, offersWithStocks?.offers]) + + const movieOffers = useMemo(() => [...dayOffers, ...nextOffers], [dayOffers, nextOffers]) + + const displayedOffers = useMemo( + () => (count === movieOffers.length ? movieOffers : movieOffers.slice(0, count)), + [count, movieOffers] + ) const increaseCount = useCallback( () => setCount((count) => { const newCount = count + nextCount - return Math.max(newCount, filteredOffers.length) + return Math.max(newCount, displayedOffers.length) }), - [filteredOffers.length, nextCount] + [displayedOffers.length, nextCount] ) const isEnd = useMemo(() => { - return displayOffers.length === filteredOffers.length - }, [displayOffers.length, filteredOffers.length]) + return displayedOffers.length === dayOffers.length + nextOffers.length + }, [displayedOffers.length, dayOffers.length, nextOffers.length]) return { - items: displayOffers, + items: displayedOffers, isLoading, increaseCount, isEnd, diff --git a/src/features/venue/components/VenueContent/VenueContent.tsx b/src/features/venue/components/VenueContent/VenueContent.tsx index 22a895796f1..2743f3bc98f 100644 --- a/src/features/venue/components/VenueContent/VenueContent.tsx +++ b/src/features/venue/components/VenueContent/VenueContent.tsx @@ -116,7 +116,10 @@ export const VenueContent: React.FunctionComponent = ({ {/* On web VenueHeader is called before Body for accessibility navigate order */} {isWeb ? : null} - + handleCheckScrollY(): number children: React.ReactNode + offset?: number } export const AnchorProvider = ({ scrollViewRef, handleCheckScrollY, + offset = 0, children, }: AnchorProviderProps) => { const { top } = useSafeAreaInsets() + const anchors = useRef>>>({}) const registerAnchor = useCallback((name: AnchorName, ref: RefObject) => { @@ -43,15 +46,16 @@ export const AnchorProvider = ({ pageY: number ) => { const currentPageScroll = handleCheckScrollY() + console.log({ pageY, currentPageScroll, height, top, offset }) scrollViewRef.current?.scrollTo({ - y: pageY + currentPageScroll - height - top, + y: pageY + currentPageScroll - height - top - offset, animated: true, }) } ) } }, - [handleCheckScrollY, scrollViewRef, top] + [handleCheckScrollY, offset, scrollViewRef, top] ) const value = useMemo( diff --git a/src/ui/components/anchor/anchor-name.ts b/src/ui/components/anchor/anchor-name.ts index 44a8819a741..a83721de0ca 100644 --- a/src/ui/components/anchor/anchor-name.ts +++ b/src/ui/components/anchor/anchor-name.ts @@ -1 +1 @@ -export type AnchorName = 'venue-calendar' +export type AnchorName = 'venue-calendar' | 'movie-calendar'