From 7ed41cceea46ff7c2effc38d6f8bab9f5eea6fe0 Mon Sep 17 00:00:00 2001 From: Mathieu Dumont Date: Wed, 2 Oct 2024 16:40:24 -0400 Subject: [PATCH] Add swiper component --- template/package.json | 1 + template/src/components/kit/swiper/Swiper.tsx | 92 +++++++++++++++++++ template/src/components/kit/swiper/hooks.ts | 62 +++++++++++++ template/src/components/kit/swiper/index.ts | 1 + template/src/components/kit/swiper/utils.ts | 22 +++++ template/src/screens/Home.tsx | 27 ++++++ template/yarn.lock | 31 ++++++- 7 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 template/src/components/kit/swiper/Swiper.tsx create mode 100644 template/src/components/kit/swiper/hooks.ts create mode 100644 template/src/components/kit/swiper/index.ts create mode 100644 template/src/components/kit/swiper/utils.ts diff --git a/template/package.json b/template/package.json index bcd0df6..622314a 100644 --- a/template/package.json +++ b/template/package.json @@ -21,6 +21,7 @@ "@react-native-firebase/remote-config": "^21.0.0", "@react-navigation/native": "^6.1.9", "@react-navigation/native-stack": "^6.9.17", + "@shopify/flash-list": "^1.7.1", "@tanstack/react-query": "^5.12.2", "axios": "^1.6.2", "babel-plugin-module-resolver": "^5.0.0", diff --git a/template/src/components/kit/swiper/Swiper.tsx b/template/src/components/kit/swiper/Swiper.tsx new file mode 100644 index 0000000..1a7c0c0 --- /dev/null +++ b/template/src/components/kit/swiper/Swiper.tsx @@ -0,0 +1,92 @@ +import { ContentStyle, FlashList, FlashListProps } from '@shopify/flash-list'; +import { View } from 'react-native'; +import Animated from 'react-native-reanimated'; +import React, { Ref, useImperativeHandle, useMemo, useRef } from 'react'; +import { useCenteredTileOffsets } from './hooks'; + +export interface SwiperProps + extends Omit< + FlashListProps, + | 'contentContainerStyle' + | 'snapToOffsets' + | 'horizontal' + | 'estimatedItemSize' + > { + containerWidth: number; + containerHorizontalPadding: number; + tileWidth: number; + gap: number; + contentContainerStyle?: Pick< + ContentStyle, + 'backgroundColor' | 'paddingVertical' | 'paddingTop' | 'paddingBottom' + >; + footerWidth?: number; + forwardedRef?: Ref; +} + +export type SwiperMethods = { scrollToTop: (animated?: boolean) => void }; +const AnimatedFlashList = Animated.createAnimatedComponent(FlashList); + +export function Swiper({ + containerWidth, + tileWidth, + gap, + containerHorizontalPadding, + contentContainerStyle, + snapToAlignment = 'start', + scrollIndicatorInsets, + footerWidth = 0, + forwardedRef, + ...props +}: SwiperProps) { + const internalRef = useRef(null); + + useImperativeHandle( + forwardedRef, + () => { + return { + scrollToTop: (animated) => { + internalRef.current?.scrollToOffset({ animated, offset: 0 }); + }, + }; + }, + [] + ); + + const offsets = useCenteredTileOffsets({ + containerWidth, + gap, + tileWidth, + tileCount: props.data?.length ?? 0, + containerHorizontalPadding, + alignment: snapToAlignment, + footerWidth, + }); + + const Gap = useMemo(() => { + return () => ; + }, [gap]); + + return ( + + ); +} diff --git a/template/src/components/kit/swiper/hooks.ts b/template/src/components/kit/swiper/hooks.ts new file mode 100644 index 0000000..6121563 --- /dev/null +++ b/template/src/components/kit/swiper/hooks.ts @@ -0,0 +1,62 @@ +import { useMemo } from 'react'; +import { getAlignmentOffset } from './utils'; + +export interface UseTileOffsetsParams { + containerWidth: number; + containerHorizontalPadding: number; + tileWidth: number; + tileCount: number; + gap: number; + alignment: 'start' | 'center' | 'end'; + footerWidth: number; +} + +export function useCenteredTileOffsets({ + containerWidth, + tileWidth, + tileCount, + containerHorizontalPadding, + gap, + alignment, + footerWidth, +}: UseTileOffsetsParams): number[] { + const paddedTileWidth = tileWidth + gap; + const alignmentOffset = getAlignmentOffset({ + alignment, + containerWidth, + containerHorizontalPadding, + tileWidth, + }); + const upperBound = + tileCount * tileWidth + + (tileCount - 1) * gap + + containerHorizontalPadding * 2 + + footerWidth - + containerWidth; + + const offsets = useMemo(() => { + const allOffsets: number[] = []; + + for (let i = 0; i < tileCount; i++) { + const startOfTile = i * paddedTileWidth + containerHorizontalPadding; + const offset = startOfTile - alignmentOffset; + + if (offset >= upperBound) { + allOffsets.push(upperBound); + break; + } + + allOffsets.push(offset); + } + + return allOffsets; + }, [ + tileCount, + upperBound, + alignmentOffset, + containerHorizontalPadding, + paddedTileWidth, + ]); + + return offsets; +} diff --git a/template/src/components/kit/swiper/index.ts b/template/src/components/kit/swiper/index.ts new file mode 100644 index 0000000..91e05d2 --- /dev/null +++ b/template/src/components/kit/swiper/index.ts @@ -0,0 +1 @@ +export * from './Swiper'; diff --git a/template/src/components/kit/swiper/utils.ts b/template/src/components/kit/swiper/utils.ts new file mode 100644 index 0000000..5a7d1f5 --- /dev/null +++ b/template/src/components/kit/swiper/utils.ts @@ -0,0 +1,22 @@ +export interface GetAlignmentOffsetParams { + alignment: 'start' | 'center' | 'end'; + containerWidth: number; + containerHorizontalPadding: number; + tileWidth: number; +} + +export function getAlignmentOffset({ + alignment, + containerWidth, + containerHorizontalPadding, + tileWidth, +}: GetAlignmentOffsetParams) { + switch (alignment) { + case 'start': + return containerHorizontalPadding; + case 'center': + return (containerWidth - tileWidth) / 2; + case 'end': + return containerWidth - (tileWidth + containerHorizontalPadding); + } +} diff --git a/template/src/screens/Home.tsx b/template/src/screens/Home.tsx index a026b9f..3cd736e 100644 --- a/template/src/screens/Home.tsx +++ b/template/src/screens/Home.tsx @@ -1,6 +1,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; import { useCallback, useEffect, useState } from 'react'; +import { useWindowDimensions } from 'react-native'; import { RootStackScreenProps } from '../navigation/RootNavigator'; import { ActivityIndicator, Button, Flex } from '../components/kit'; import { useToaster, Text } from '~/components/kit'; @@ -12,12 +13,14 @@ import Geolocation, { import { useApplicationConfiguration } from '~/hooks/use-application-configuration'; import { useRemoteConfig } from '~/hooks/use-remote-config'; import { useNewFeature } from '~/hooks/use-new-feature'; +import { Swiper } from '~/components/kit/swiper'; export type HomeScreenProps = RootStackScreenProps<'Home'>; export function HomeScreen({ navigation }: HomeScreenProps) { const geolocation = useService(Geolocation); const { i18n } = useTranslation(); + const { width } = useWindowDimensions(); const apiUrl = useApplicationConfiguration('API_URL'); const secretPanelEnabled = useApplicationConfiguration( @@ -110,6 +113,30 @@ export function HomeScreen({ navigation }: HomeScreenProps) { )} + + ( + + + {item} + + + )} + /> ); } diff --git a/template/yarn.lock b/template/yarn.lock index 3f22b73..090d6b2 100644 --- a/template/yarn.lock +++ b/template/yarn.lock @@ -2185,6 +2185,14 @@ dependencies: nanoid "^3.1.23" +"@shopify/flash-list@^1.7.1": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@shopify/flash-list/-/flash-list-1.7.1.tgz#11644551d86b8a9ef83f521487bebba478bfc011" + integrity sha512-sUYl7h8ydJutufA26E42Hj7cLvaBTpkMIyNJiFrxUspkcANb6jnFiLt9rEwAuDjvGk/C0lHau+WyT6ZOxqVPwg== + dependencies: + recyclerlistview "4.2.1" + tslib "2.6.3" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -5543,7 +5551,7 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" -lodash.debounce@^4.0.8: +lodash.debounce@4.0.8, lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== @@ -6472,7 +6480,7 @@ prompts@^2.0.1, prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.7.2, prop-types@^15.8.1: +prop-types@15.8.1, prop-types@^15.7.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -6797,6 +6805,15 @@ recast@^0.21.0: source-map "~0.6.1" tslib "^2.0.1" +recyclerlistview@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/recyclerlistview/-/recyclerlistview-4.2.1.tgz#4537a0959400cdce1df1f38d26aab823786e9b13" + integrity sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g== + dependencies: + lodash.debounce "4.0.8" + prop-types "15.8.1" + ts-object-utils "0.0.5" + redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -7535,6 +7552,11 @@ ts-node@^10.9.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +ts-object-utils@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/ts-object-utils/-/ts-object-utils-0.0.5.tgz#95361cdecd7e52167cfc5e634c76345e90a26077" + integrity sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA== + tsconfig-paths@^3.14.2: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" @@ -7545,6 +7567,11 @@ tsconfig-paths@^3.14.2: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tslib@^1.8.1, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"