diff --git a/app.json b/app.json index 7f49cf6b0..ff8e370c6 100644 --- a/app.json +++ b/app.json @@ -77,8 +77,6 @@ [ "@sentry/react-native/expo", { - "url": "https://sentry.io/", - "note": "Use SENTRY_AUTH_TOKEN env to authenticate with Sentry.", "project": "vox", "organization": "parti-renaissance" } diff --git a/app/(tabs)/(home)/_layout.tsx b/app/(tabs)/(home)/_layout.tsx index 1f9660117..450d33c94 100644 --- a/app/(tabs)/(home)/_layout.tsx +++ b/app/(tabs)/(home)/_layout.tsx @@ -1,3 +1,5 @@ +import EuCampaignIllustration from '@/assets/illustrations/EuCampaignIllustration' +import Header, { SmallHeader } from '@/components/Header/Header' import { useSession } from '@/ctx/SessionProvider' import { Redirect, Stack } from 'expo-router' @@ -7,12 +9,13 @@ export default function AppLayout() { if (!isAuth) { return } + const config = { title: '' } return ( - - - - - + + }} /> + + + ) } diff --git a/app/(tabs)/(home)/index.tsx b/app/(tabs)/(home)/index.tsx index 784fec2fb..4e71a9e38 100644 --- a/app/(tabs)/(home)/index.tsx +++ b/app/(tabs)/(home)/index.tsx @@ -8,26 +8,13 @@ import MyProfileCard from '@/components/ProfileCards/ProfileCard/MyProfileCard' import ProgramCTA from '@/components/ProfileCards/ProgramCTA/ProgramCTA' import SkeCard from '@/components/Skeleton/CardSkeleton' import * as metatags from '@/config/metatags' -import { useSession } from '@/ctx/SessionProvider' import HomeFeedList from '@/screens/home/feed/HomeFeedList' -import { Redirect, Stack as RouterStack } from 'expo-router' import Head from 'expo-router/head' import { YStack } from 'tamagui' const HomeScreen: React.FC = () => { - const { isAuth } = useSession() - - if (!isAuth) { - return - } - return ( <> - {metatags.createTitle('Le fil')} diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 82c1de57c..10cbf8365 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -1,16 +1,18 @@ import React from 'react' import { Pressable } from 'react-native' import { useSafeAreaInsets } from 'react-native-safe-area-context' -import NavBar from '@/components/Header/Header' +import Text from '@/components/base/Text' import WaitingScreen from '@/components/WaitingScreen' import { ROUTES } from '@/config/routes' import { useSession } from '@/ctx/SessionProvider' import useInit from '@/hooks/useInit' +import { getFocusedRouteNameFromRoute } from '@react-navigation/native' +import type { Route } from '@react-navigation/routers' import { parse, useURL } from 'expo-linking' -import { Tabs, useGlobalSearchParams } from 'expo-router' +import { Tabs, useGlobalSearchParams, useSegments } from 'expo-router' import { isWeb, useMedia, View } from 'tamagui' -const TAB_BAR_HEIGTH = 60 +const TAB_BAR_HEIGTH = 80 export default function AppLayout() { const insets = useSafeAreaInsets() @@ -22,6 +24,12 @@ export default function AppLayout() { useInit() + const segments = useSegments() + const getTabBarVisibility = () => { + const hideOnScreens = ['tunnel', 'building-detail', 'polls'] // put here name of screen where you want to hide tabBar + return hideOnScreens.map((screen) => segments.includes(screen)).some(Boolean) + } + if (!isAuth && !isLoading && (code || url)) { if (isWeb && code) { signIn({ code }) @@ -41,18 +49,31 @@ export default function AppLayout() { return ( , + headerShown: false, tabBarLabel: () => null, tabBarLabelPosition: 'below-icon', tabBarButton: (props) => , + tabBarHideOnKeyboard: true, + headerShadowVisible: false, tabBarStyle: { backgroundColor: 'white', - borderTopWidth: 2, - borderTopColor: 'rgba(145, 158, 171, 0.32)', - display: media.gtSm || !session ? 'none' : 'flex', + borderTopWidth: 1, + shadowOffset: { width: 0, height: 0 }, + elevation: 0, + borderTopColor: 'rgba(145, 158, 171, 0.2)', + display: media.gtSm || !session || getTabBarVisibility() ? 'none' : 'flex', height: TAB_BAR_HEIGTH + insets.bottom, + alignContent: 'center', + justifyContent: 'center', + padding: 0, + paddingTop: 15, + }, + + tabBarItemStyle: { + paddingBottom: 20, + // paddingTop: 0, }, }} > @@ -62,11 +83,13 @@ export default function AppLayout() { name={route.name} options={{ href: route.hidden === true ? null : undefined, - tabBarLabel: route.screenName, + title: route.screenName, + tabBarLabel: ({ focused }) => ( + + {route.screenName} + + ), tabBarActiveTintColor: route.labelColor, - tabBarLabelStyle: { - marginBottom: 5, - }, tabBarIcon: ({ focused }) => { const Icon = ({ focused }) => diff --git a/app/(tabs)/actions/_layout.tsx b/app/(tabs)/actions/_layout.tsx index d2583b764..345af8d53 100644 --- a/app/(tabs)/actions/_layout.tsx +++ b/app/(tabs)/actions/_layout.tsx @@ -1,10 +1,10 @@ -import { headerBlank } from '@/styles/navigationAppearance' +import { SmallHeader } from '@/components/Header/Header' import { Stack } from 'expo-router' export default function AppLayout() { return ( - - + + diff --git a/app/(tabs)/evenements/[id].tsx b/app/(tabs)/evenements/[id].tsx index 23b23ef47..6057aa966 100644 --- a/app/(tabs)/evenements/[id].tsx +++ b/app/(tabs)/evenements/[id].tsx @@ -1,4 +1,5 @@ import React from 'react' +import Error404 from '@/components/404/Error404' import BoundarySuspenseWrapper from '@/components/BoundarySuspenseWrapper' import PageLayout from '@/components/layouts/PageLayout/PageLayout' import AppDownloadCTA from '@/components/ProfileCards/AppDownloadCTA/AppDownloadCTA' @@ -15,6 +16,7 @@ import { isWeb, YStack } from 'tamagui' const HomeScreen: React.FC = () => { const params = useLocalSearchParams<{ id: string }>() + if (!params.id) return return ( diff --git a/app/(tabs)/evenements/_layout.tsx b/app/(tabs)/evenements/_layout.tsx index 88d277e9f..d4208ec36 100644 --- a/app/(tabs)/evenements/_layout.tsx +++ b/app/(tabs)/evenements/_layout.tsx @@ -1,11 +1,18 @@ -import { headerBlank } from '@/styles/navigationAppearance' +import EuCampaignIllustration from '@/assets/illustrations/EuCampaignIllustration' +import Header, { SmallHeader } from '@/components/Header/Header' +import { useSession } from '@/ctx/SessionProvider' import { Stack } from 'expo-router' -export default function AppLayout() { +export default function EventLayout() { + const { isAuth } = useSession() return ( - - + , animation: 'slide_from_right' }}> +
, title: 'événements', headerLeft: () => }} + /> + ) } diff --git a/app/(tabs)/evenements/index.tsx b/app/(tabs)/evenements/index.tsx index e42bfd72b..8eeb6e1a8 100644 --- a/app/(tabs)/evenements/index.tsx +++ b/app/(tabs)/evenements/index.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useState } from 'react' import BoundarySuspenseWrapper from '@/components/BoundarySuspenseWrapper' import BottomSheetFilter from '@/components/EventFilterForm/BottomSheetFilters' import EventFilterForm from '@/components/EventFilterForm/EventFilterForm' @@ -9,21 +9,17 @@ import MyProfileCard from '@/components/ProfileCards/ProfileCard/MyProfileCard' import ProfileLoginCTA from '@/components/ProfileCards/ProfileLoginCTA/ProfileLoginCTA' import AuthFallbackWrapper from '@/components/Skeleton/AuthFallbackWrapper' import SkeCard from '@/components/Skeleton/CardSkeleton' +import { Tabs } from '@/components/Tabs/Tabs' import * as metatags from '@/config/metatags' import EventFeedList from '@/screens/events/EventFeedList' -import { Stack as RouterStack } from 'expo-router' import Head from 'expo-router/head' -import { YStack } from 'tamagui' +import { useMedia, YStack } from 'tamagui' const EventsScreen: React.FC = () => { + const [activeTab, setActiveTab] = useState<'events' | 'myEvents'>('events') + const media = useMedia() return ( <> - - {metatags.createTitle('Nos événements')} @@ -41,6 +37,16 @@ const EventsScreen: React.FC = () => { + + value={activeTab} + onChange={setActiveTab} + grouped={media.lg} + $gtMd={{ paddingHorizontal: '$7', paddingTop: '$6', paddingBottom: 0 }} + > + Tous les événements + J'y participe + + @@ -89,7 +95,7 @@ const EventsScreen: React.FC = () => { } > - + diff --git a/app/(tabs)/porte-a-porte/_layout.tsx b/app/(tabs)/porte-a-porte/_layout.tsx index 963eab101..7cf7fb8c8 100644 --- a/app/(tabs)/porte-a-porte/_layout.tsx +++ b/app/(tabs)/porte-a-porte/_layout.tsx @@ -1,15 +1,18 @@ -import MobileWallLayout from '@/components/MobileWallLayout/MobileWallLayout' -import { headerBlank } from '@/styles/navigationAppearance' +import { SmallHeader } from '@/components/Header/Header' import { Stack } from 'expo-router' -import { isWeb } from 'tamagui' export default function DoorToDoorLayout() { - return isWeb ? ( - - ) : ( - - - + return ( + + + + ) } diff --git a/app/(tabs)/porte-a-porte/building-detail.tsx b/app/(tabs)/porte-a-porte/building-detail.tsx index 380c8272d..772949e14 100644 --- a/app/(tabs)/porte-a-porte/building-detail.tsx +++ b/app/(tabs)/porte-a-porte/building-detail.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useLayoutEffect, useState } from 'react' -import { Image, Modal, RefreshControl, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native' +import { Modal, RefreshControl, SafeAreaView, ScrollView, StyleSheet, Text, View } from 'react-native' import { BuildingBlock, BuildingBlockHelper } from '@/core/entities/BuildingBlock' import { BuildingHistoryPoint } from '@/core/entities/BuildingHistory' import { BuildingType, DoorToDoorAddress } from '@/core/entities/DoorToDoor' @@ -27,7 +27,6 @@ import AlphabetHelper from '@/utils/AlphabetHelper' import i18n from '@/utils/i18n' import { useIsFocused } from '@react-navigation/native' import { router, useNavigation } from 'expo-router' -import { last } from 'lodash' enum Tab { HISTORY, @@ -44,7 +43,7 @@ const BuildingDetailScreen = () => { const [campaignCardViewModel, setCampaignCardViewModel] = useState() const dtdStore = useDoorToDoorStore() const { setTunnel } = useDtdTunnelStore() - const [address, setAddress] = useState(dtdStore.address) + const [address, setAddress] = useState(dtdStore.address as DoorToDoorAddress) const [rankingModalState, setRankingModalState] = useState({ visible: false }) const viewModel = BuildingDetailScreenViewModelMapper.map(address!, history, layout) const buildingBlockHelper = new BuildingBlockHelper() @@ -59,6 +58,9 @@ const BuildingDetailScreen = () => { }) const fetchLayout = async (): Promise> => { + if (!dtdStore.address) { + throw new Error('No address found') + } return await new UpdateBuildingLayoutInteractor().execute( dtdStore.address.building.id, campaignStatistics.campaignId, @@ -68,6 +70,9 @@ const BuildingDetailScreen = () => { } const fetchHistory = async (): Promise> => { + if (!dtdStore.address) { + throw new Error('No address found') + } return await DoorToDoorRepository.getInstance().buildingHistory(dtdStore.address.building.id, campaignStatistics.campaignId) } @@ -284,7 +289,7 @@ const BuildingDetailScreen = () => { campaignId: campaignStatistics.campaignId, buildingParams: { id: address.building.id, - block: block?.name, + block: block?.name!, floor: floorNumber, type: address.building.type, door: door, diff --git a/app/(tabs)/porte-a-porte/index.tsx b/app/(tabs)/porte-a-porte/index.tsx index 223f76067..7f1eb6944 100644 --- a/app/(tabs)/porte-a-porte/index.tsx +++ b/app/(tabs)/porte-a-porte/index.tsx @@ -1,6 +1,7 @@ import React, { memo, useCallback, useState } from 'react' -import { Modal, SafeAreaView, StyleSheet, Text, View } from 'react-native' +import { Modal, SafeAreaView, StyleSheet, View } from 'react-native' import { LatLng, Region } from '@/components/Maps/Maps' +import MobileWallLayout from '@/components/MobileWallLayout/MobileWallLayout' import { DoorToDoorCharterNotAccepted } from '@/core/entities/DoorToDoorCharterState' import { GetDoorToDoorAddressesInteractor } from '@/core/interactor/GetDoorToDoorAddressesInteractor' import DoorToDoorRepository from '@/data/DoorToDoorRepository' @@ -19,7 +20,8 @@ import i18n from '@/utils/i18n' import { useOnFocus } from '@/utils/useOnFocus.hook' import { useQuery } from '@tanstack/react-query' import * as Geolocation from 'expo-location' -import { router } from 'expo-router' +import { router, useRootNavigationState } from 'expo-router' +import { isWeb } from 'tamagui' const DoorToDoorMapView = memo(_DoorToDoorMapView) @@ -162,12 +164,12 @@ const DoorToDoorScreen = () => { )} - + {/* {i18n.t('doorToDoor.title')} {!error && } - + */} {renderContent()} ) @@ -198,4 +200,4 @@ const styles = StyleSheet.create({ }, }) -export default DoorToDoorScreen +export default isWeb ? MobileWallLayout : DoorToDoorScreen diff --git a/app/(tabs)/porte-a-porte/tunnel/_layout.tsx b/app/(tabs)/porte-a-porte/tunnel/_layout.tsx index fa46b3c16..0d511cc38 100644 --- a/app/(tabs)/porte-a-porte/tunnel/_layout.tsx +++ b/app/(tabs)/porte-a-porte/tunnel/_layout.tsx @@ -1,7 +1,7 @@ -import React, { FunctionComponent } from 'react' +import React, { FunctionComponent, useEffect } from 'react' import { headerBlank } from '@/styles/navigationAppearance' import i18n from '@/utils/i18n' -import { Stack } from 'expo-router' +import { Stack, useNavigation, useRootNavigationState } from 'expo-router' const DoorToDoorTunnelModalNavigator: FunctionComponent = () => { return ( diff --git a/app/(tabs)/profil/_layout.tsx b/app/(tabs)/profil/_layout.tsx index ca01fd3e1..aa2bc4a40 100644 --- a/app/(tabs)/profil/_layout.tsx +++ b/app/(tabs)/profil/_layout.tsx @@ -1,5 +1,5 @@ +import { SmallHeader } from '@/components/Header/Header' import { useSession } from '@/ctx/SessionProvider' -import { headerBlank } from '@/styles/navigationAppearance' import i18n from '@/utils/i18n' import { Redirect, Stack } from 'expo-router' @@ -11,8 +11,8 @@ export default function AppLayout() { } return ( - - + + diff --git a/app/(tabs)/profil/index.tsx b/app/(tabs)/profil/index.tsx index 21c049ea1..5b4fb0ddc 100644 --- a/app/(tabs)/profil/index.tsx +++ b/app/(tabs)/profil/index.tsx @@ -16,11 +16,7 @@ function ProfilScreen() { {metatags.createTitle('Mon profil')} - + diff --git a/app/(tabs)/ressources/_layout.tsx b/app/(tabs)/ressources/_layout.tsx index 5311b9c64..6f1ba3b02 100644 --- a/app/(tabs)/ressources/_layout.tsx +++ b/app/(tabs)/ressources/_layout.tsx @@ -1,5 +1,5 @@ +import { SmallHeader } from '@/components/Header/Header' import { useSession } from '@/ctx/SessionProvider' -import { headerBlank } from '@/styles/navigationAppearance' import { Redirect, Stack } from 'expo-router' export default function ActionsScreen() { @@ -10,8 +10,8 @@ export default function ActionsScreen() { } return ( - - + + ) } diff --git a/app/(tabs)/ressources/index.tsx b/app/(tabs)/ressources/index.tsx index 5eb0981bb..9d27f22da 100644 --- a/app/(tabs)/ressources/index.tsx +++ b/app/(tabs)/ressources/index.tsx @@ -6,7 +6,6 @@ import MyProfileCard from '@/components/ProfileCards/ProfileCard/MyProfileCard' import SkeCard from '@/components/Skeleton/CardSkeleton' import * as metatags from '@/config/metatags' import ResourcesList from '@/screens/tools/ResourcesList' -import { Stack as RouterStack } from 'expo-router' import Head from 'expo-router/head' import { View, YStack } from 'tamagui' @@ -43,12 +42,6 @@ const ToolsSkeleton = () => { const ToolsScreen: React.FC = () => { return ( <> - - {metatags.createTitle('Ressources')} diff --git a/app/[...unmatched].tsx b/app/[...unmatched].tsx index 2e329b5c4..7c278f650 100644 --- a/app/[...unmatched].tsx +++ b/app/[...unmatched].tsx @@ -2,18 +2,15 @@ import { useColorScheme } from 'react-native' import Error404 from '@/components/404/Error404' import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' import { TamaguiProvider } from '@tamagui/core' -import { Unmatched } from 'expo-router' -// export default function UnmatchedRoute() { -// const colorScheme = useColorScheme() +export default function UnmatchedRoute() { + const colorScheme = useColorScheme() -// return ( -// -// -// -// -// -// ) -// } - -export default Unmatched + return ( + + + + + + ) +} diff --git a/package.json b/package.json index 5ea454b5c..32d16bde9 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "country-data": "^0.0.31", "date-fns": "^3.6.0", "date-fns-tz": "^3.1.3", - "expo": "~51.0.7", + "expo": "~51.0.8", "expo-application": "~5.9.1", "expo-auth-session": "~5.5.2", "expo-blur": "~13.0.2", @@ -90,7 +90,7 @@ "i18next": "^23.7.16", "intl-pluralrules": "^2.0.1", "jest": "^29.3.1", - "jest-expo": "~51.0.1", + "jest-expo": "~51.0.2", "jotai": "^2.6.2", "ky": "0.28.7", "localized-address-format": "^1.3.1", diff --git a/src/assets/icons/nav/DoorIcon.tsx b/src/assets/icons/nav/DoorIcon.tsx index a1e665d71..1e46566bc 100644 --- a/src/assets/icons/nav/DoorIcon.tsx +++ b/src/assets/icons/nav/DoorIcon.tsx @@ -37,13 +37,13 @@ const Icon = (props) => { fillRule="evenodd" clipRule="evenodd" d="M15 10.5a.488.488 0 01-.5.488l-3.1-.078a.41.41 0 010-.82l3.1-.077c.274-.007.5.213.5.487z" - fill={getFillUrl(3)} + fill={getFillUrl(2)} /> @@ -61,10 +61,6 @@ const Icon = (props) => { - - - - ))} diff --git a/src/assets/icons/nav/ToolsIcon.tsx b/src/assets/icons/nav/ToolsIcon.tsx index f7a73cf95..c0766b5a3 100644 --- a/src/assets/icons/nav/ToolsIcon.tsx +++ b/src/assets/icons/nav/ToolsIcon.tsx @@ -4,8 +4,8 @@ import type { IconProps } from '@tamagui/helpers-icon' import { themed } from '@tamagui/helpers-icon' const inactiveColors = [ - ['#AEB9C3', '#848E9B'], ['#D7DCE1', '#C1C7CD'], + ['#AEB9C3', '#848E9B'], ] const activeColors = [ diff --git a/src/components/AutoSizeImage.tsx b/src/components/AutoSizeImage.tsx index 39f41a2a0..f4a2698b3 100644 --- a/src/components/AutoSizeImage.tsx +++ b/src/components/AutoSizeImage.tsx @@ -1,5 +1,6 @@ import { useMemo, useState } from 'react' import { StyleSheet, TouchableOpacity, View } from 'react-native' +import Animated from 'react-native-reanimated' import { FontAwesome } from '@expo/vector-icons' import { Image } from 'expo-image' @@ -24,7 +25,7 @@ function AutoSizeImage(props: AutoSizeImageProps) { : undefined return ( - + )} - + ) } diff --git a/src/components/Cards/EventCard/EventCard.tsx b/src/components/Cards/EventCard/EventCard.tsx index 58c96b437..5dfaa72d0 100644 --- a/src/components/Cards/EventCard/EventCard.tsx +++ b/src/components/Cards/EventCard/EventCard.tsx @@ -6,7 +6,7 @@ import VoxCard, { VoxCardAuthorProps, VoxCardDateProps, VoxCardFrameProps, VoxCa import { useSession } from '@/ctx/SessionProvider' import { useSubscribeEvent, useUnsubscribeEvent } from '@/hooks/useEvents' import { router } from 'expo-router' -import { XStack } from 'tamagui' +import { Spinner, XStack } from 'tamagui' import { useDebouncedCallback } from 'use-debounce' type VoxCardBasePayload = { @@ -40,14 +40,15 @@ export const SubscribeEventButton = ({ ...btnProps }: { eventId: string; isSubscribed: boolean; outside?: boolean } & ComponentProps) => { const { session } = useSession() - const { mutate: subscribe } = useSubscribeEvent({ id }) - const { mutate: unsubscribe } = useUnsubscribeEvent({ id }) + const { mutate: subscribe, isPending: isSubPending } = useSubscribeEvent({ id }) + const { mutate: unsubscribe, isPending: isUnSubPending } = useUnsubscribeEvent({ id }) const handleSubscribe = useDebouncedCallback(() => (isSubscribed ? unsubscribe() : subscribe()), 200) const outsideStyle = outside ? ({ size: 'lg', width: '100%' } as const) : {} return isSubscribed ? ( ) : ( @@ -66,6 +67,7 @@ export const SubscribeEventButton = ({ {...outsideStyle} > M'inscrire + {isSubPending ? : null} ) } diff --git a/src/components/Cards/NewsCard/NewsCard.tsx b/src/components/Cards/NewsCard/NewsCard.tsx index 4b3227e53..059d992bd 100644 --- a/src/components/Cards/NewsCard/NewsCard.tsx +++ b/src/components/Cards/NewsCard/NewsCard.tsx @@ -26,10 +26,10 @@ const NewsCard = ({ payload, onShare, onShow, ...props }: NewsVoxCardProps) => { {payload.title} - {payload.image && } + {!!payload.image && } {payload.description} - {payload.author.name && } - {payload.ctaLabel && ( + {!!payload.author.name && } + {!!payload.ctaLabel && ( + ) : ( - + ) }, []) @@ -29,6 +29,8 @@ const SearchBox = forwardRef(({ value, onChange, onFo placeholder="Rechercher un événement" size={'$4'} ref={searchInputRef} + backgroundColor={'$white1'} + placeholderTextColor={'$textSecondary'} value={value} onFocus={onFocus} iconRight={ 0} />} diff --git a/src/components/EventRegistrationSuccess/Assets/vote-box.png b/src/components/EventRegistrationSuccess/Assets/vote-box.png new file mode 100644 index 000000000..199290cf5 Binary files /dev/null and b/src/components/EventRegistrationSuccess/Assets/vote-box.png differ diff --git a/src/components/EventRegistrationSuccess/EventRegistrationSuccess.stories.tsx b/src/components/EventRegistrationSuccess/EventRegistrationSuccess.stories.tsx new file mode 100644 index 000000000..d2a8ba4db --- /dev/null +++ b/src/components/EventRegistrationSuccess/EventRegistrationSuccess.stories.tsx @@ -0,0 +1,13 @@ +import EventRegistrationSuccess from '@/components/EventRegistrationSuccess/EventRegistrationSuccess' +import { StoryObj } from '@storybook/react' + +const meta = { + title: 'Modal/Event Registration Success', + component: EventRegistrationSuccess, +} + +type Story = StoryObj + +export const Default: Story = {} + +export default meta diff --git a/src/components/EventRegistrationSuccess/EventRegistrationSuccess.tsx b/src/components/EventRegistrationSuccess/EventRegistrationSuccess.tsx new file mode 100644 index 000000000..0977e6eb5 --- /dev/null +++ b/src/components/EventRegistrationSuccess/EventRegistrationSuccess.tsx @@ -0,0 +1,82 @@ +import React, { NamedExoticComponent } from 'react' +import Text from '@/components/base/Text' +import Button from '@/components/Button' +import { IconProps } from '@tamagui/helpers-icon' +import { CalendarDays, Clock, MapPin, UserCheck } from '@tamagui/lucide-icons' +import { Link } from 'expo-router' +import { Dialog, Image, Separator, View } from 'tamagui' + +interface EventRegistrationSuccessProps { + onClose?: () => void + open: boolean +} + +export default function EventRegistrationSuccess({ onClose, open }: EventRegistrationSuccessProps) { + return ( + + + + + + + + Félicitations vous êtes bien inscrit ! + Un mail récapitulatif vient de vous être envoyé. + + + + + Grand Meeting de Lille • + Lancement de campagne (34090) + + + + + + + + + + + + + + + + + + + + + ) +} + +const EventEntry = ({ text, captionText, Icon }: { text: string; captionText?: string; Icon: NamedExoticComponent }) => ( + + + + + + + {text} {captionText ? {`• ${captionText}`} : null} + + + +) diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index b9af0c7a2..670e17919 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -5,10 +5,11 @@ import EuCampaignIllustration from '@/assets/illustrations/EuCampaignIllustratio import ProfilePopover from '@/components/ProfilePopover/ProfilePopover' import { ROUTES } from '@/config/routes' import { useSession } from '@/ctx/SessionProvider' -import { BottomTabHeaderProps } from '@react-navigation/bottom-tabs/src/types' +import { NativeStackHeaderProps } from '@react-navigation/native-stack' import { ArrowLeft, ChevronDown } from '@tamagui/lucide-icons' -import { Link, usePathname, useSegments } from 'expo-router' -import { Button, Circle, Spinner, Stack, StackProps, styled, useMedia, View } from 'tamagui' +import { Link, usePathname } from 'expo-router' +import { capitalize } from 'lodash' +import { Button, isWeb, Spinner, Stack, styled, useMedia, View, XStack, YStackProps } from 'tamagui' import Text from '../base/Text' import { SignInButton, SignUpButton } from '../Buttons/AuthButton' import Container from '../layouts/Container' @@ -30,12 +31,15 @@ const ButtonNav = styled(Button, { const NavItem = (props: { route: (typeof ROUTES)[number]; isActive: boolean }) => { const colorOpacity = opacityToHexCode(props.route.gradiant[0], 0.09) + const [isHover, setIsHover] = React.useState(false) return ( setIsHover(true)} + onHoverOut={() => setIsHover(false)} animation="bouncy" hoverStyle={{ - bg: props.isActive ? colorOpacity : 'transparent', + bg: colorOpacity, bc: 'transparent', }} pressStyle={{ @@ -44,7 +48,7 @@ const NavItem = (props: { route: (typeof ROUTES)[number]; isActive: boolean }) = }} > - + @@ -57,7 +61,7 @@ const NavItem = (props: { route: (typeof ROUTES)[number]; isActive: boolean }) = const MemoizedNavItem = React.memo(NavItem) -const NavBar = () => { +export const NavBar = () => { const pathname = usePathname() const { gtSm } = useMedia() const { session } = useSession() @@ -80,12 +84,12 @@ const ProfileView = () => { {!user.isLoading ? ( <> - + {profile?.first_name} {profile?.last_name} - + ) : ( @@ -93,7 +97,6 @@ const ProfileView = () => { ) } - const LoginView = () => ( @@ -103,49 +106,74 @@ const LoginView = () => ( ) -const Header: React.FC = (props: BottomTabHeaderProps) => { - const segments = useSegments() - const isNested = segments.length > 2 - const backPath = segments - .filter((x: string) => !x.startsWith('(')) - .slice(0, -1) - .join('/') +export const ProfileNav = () => { + return ( + }> + + + + + + + + ) +} + +const Header = (_props: NativeStackHeaderProps & YStackProps) => { + const { options, navigation, back, ...props } = _props + const media = useMedia() const BackBtn = () => ( - - - - - - - + navigation.goBack()}> + + + + + + {back?.title ?? 'Retour'} + + + ) + + const LeftNav = () => { + if (options.headerLeft) return options.headerLeft({ label: back?.title, canGoBack: navigation.canGoBack() }) + if (navigation.canGoBack() && navigation.getState().index > 0) { + return + } + return media.gtSm && isWeb ? ( + + ) : ( + + {capitalize(options.title)} + + ) + } return ( - + - {isNested ? ( - - ) : ( - - - - )} - {!isNested && } - }> - - - - - - - + + {!(navigation.canGoBack() && navigation.getState().index > 0) && } + {options.headerRight ? options.headerRight({ canGoBack: navigation.canGoBack() }) : } ) } +export const SmallHeader: typeof Header = (props) => { + const media = useMedia() + return
+} + export default Header diff --git a/src/components/Tabs/Tab.tsx b/src/components/Tabs/Tab.tsx new file mode 100644 index 000000000..77e8be48c --- /dev/null +++ b/src/components/Tabs/Tab.tsx @@ -0,0 +1,75 @@ +import { styled, YStack, YStackProps } from 'tamagui' +import Text from '../base/Text' + +export type TabProps = { + children: string | string[] + active?: boolean + grouped?: boolean + onPress?: () => void +} & YStackProps + +const TabContainer = styled(YStack, { + cursor: 'pointer', + height: 48, + justifyContent: 'center', + alignItems: 'center', + borderBottomWidth: 2, + borderBottomColor: 'transparent', + animation: 'medium', + + hoverStyle: { + borderBottomColor: '$textSecondary', + }, + pressStyle: { + backgroundColor: '$gray4', + }, + variants: { + grouped: { + true: { + justifyContent: 'center', + flex: 1, + }, + }, + active: { + true: { + borderBottomColor: '$textPrimary', + }, + }, + }, +} as const) + +const TabText = styled(Text, { + color: '$textSecondary', + cursor: 'pointer', + hoverStyle: { + color: '$textSecondary', + }, + pressStyle: { + color: '$textPrimary', + }, + variants: { + active: { + true: { + color: '$textPrimary', + hoverStyle: { + color: '$textPrimary', + }, + }, + }, + grouped: { + true: { + textAlign: 'center', + }, + }, + }, +} as const) + +export default function Tab({ children, active, onPress, grouped, ...rest }: TabProps & YStackProps) { + return ( + + + {children} + + + ) +} diff --git a/src/components/Tabs/Tabs.stories.tsx b/src/components/Tabs/Tabs.stories.tsx new file mode 100644 index 000000000..67e440fce --- /dev/null +++ b/src/components/Tabs/Tabs.stories.tsx @@ -0,0 +1,34 @@ +import { useState } from 'react' +import { Lock } from '@tamagui/lucide-icons' +import { Stack } from 'tamagui' +import { Tabs } from './Tabs' + +export default { + title: 'Tabs', + component: Tabs, +} + +export function Default() { + const [value, setValue] = useState<'1' | '2' | '3'>('1') + return ( + + onChange={setValue} value={value}> + Tab 1 + Tab 2 + Tab 3 + + + ) +} +export function grouped() { + const [value, setValue] = useState<'1' | '2' | '3'>('1') + return ( + + grouped onChange={setValue} value={value}> + Tab 1 + Tab 2 + Tab 3 + + + ) +} diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx new file mode 100644 index 000000000..db9c08b20 --- /dev/null +++ b/src/components/Tabs/Tabs.tsx @@ -0,0 +1,56 @@ +import { createContext, memo, ReactElement, SetStateAction, useCallback, useContext, useState } from 'react' +import { styled, withStaticProperties, XStack, YStackProps } from 'tamagui' +import _Tab, { TabProps } from './Tab' + +const TabMemo = memo(_Tab) + +export const TabsContext = createContext({ + activeTab: '', + grouped: false as boolean | undefined, + setActiveTab: (id: string) => {}, +}) + +type TabsFrameProps = YStackProps & { + onChange: (id: A) => void + value: A + children: ReactElement<_TabProps>[] + grouped?: boolean +} + +const StyledFrame = styled(XStack, { + gap: '$3', + padding: '$3.5', + variants: { + grouped: { + true: { + bg: '$white1', + gap: 0, + padding: 0, + }, + }, + }, +} as const) + +function TabsFrame({ children, value, onChange, grouped, ...rest }: TabsFrameProps) { + return ( + + + {children} + + + ) +} + +type _TabProps = { + id: A +} & TabProps + +export const Tab = (props: _TabProps) => { + const ctx = useContext(TabsContext) + const handlePress = useCallback(() => ctx.setActiveTab(props.id), [props.id]) + return +} + +export const Tabs = withStaticProperties(TabsFrame, { + Tab, +}) diff --git a/src/components/VoxCard/VoxCard.tsx b/src/components/VoxCard/VoxCard.tsx index 37a51f615..439fdd3a5 100644 --- a/src/components/VoxCard/VoxCard.tsx +++ b/src/components/VoxCard/VoxCard.tsx @@ -15,6 +15,8 @@ const CardFrame = styled(YStack, { backgroundColor: '$white1', $gtSm: { borderRadius: '$8', + borderColor: '$gray3', + borderWidth: 1, }, } as const) diff --git a/src/components/layouts/Container.tsx b/src/components/layouts/Container.tsx index fc538202f..af3bccaf4 100644 --- a/src/components/layouts/Container.tsx +++ b/src/components/layouts/Container.tsx @@ -1,11 +1,11 @@ -import { getConfig, getMedia, getTokenValue, mediaObjectToString, mediaQueryConfig, Stack, StackProps } from 'tamagui' +import { StackProps, YStack, YStackProps } from 'tamagui' -export default function Container({ children, ...props }: StackProps) { +export default function Container({ children, ...props }: YStackProps) { return ( - - + + {children} - - + + ) } diff --git a/src/config/routes.ts b/src/config/routes.ts index a4b581887..f9dec1b18 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -13,7 +13,7 @@ export type TabRoute = { export const ROUTES: TabRoute[] = [ { name: '(home)', - screenName: 'Fil', + screenName: 'Mon fil', href: '/', icon: HomeIcon, gradiant: ['#8D98FF', '#8050E6'], diff --git a/src/data/mapper/GetEventsSearchParametersMapper.ts b/src/data/mapper/GetEventsSearchParametersMapper.ts index 9a1e43265..8b7486982 100644 --- a/src/data/mapper/GetEventsSearchParametersMapper.ts +++ b/src/data/mapper/GetEventsSearchParametersMapper.ts @@ -6,6 +6,7 @@ type GetEventsSearchParametersMapperPropsBase = { page: number filters: EventFilters | undefined orderBySubscriptions?: boolean + orderByBeginAt?: boolean } export type GetEventsSearchParametersMapperProps = @@ -23,7 +24,7 @@ export type GetEventsSearchParametersMapperProps = } & GetEventsSearchParametersMapperPropsBase) export const GetEventsSearchParametersMapper = { - map: ({ page, zipCode, zoneCode, filters, orderBySubscriptions }: GetEventsSearchParametersMapperProps): SearchParamsKeyValue => { + map: ({ page, zipCode, zoneCode, filters, orderBySubscriptions, orderByBeginAt }: GetEventsSearchParametersMapperProps): SearchParamsKeyValue => { let searchParams: SearchParamsKeyValue = { page } const subscribedOnly = filters?.subscribedOnly ? true : undefined if (subscribedOnly) { @@ -65,6 +66,13 @@ export const GetEventsSearchParametersMapper = { ...searchParams, } } + + if (orderByBeginAt) { + searchParams = { + 'order[beginAt]': 'desc', + ...searchParams, + } + } return searchParams }, } diff --git a/src/data/restObjects/RestTimelineFeedResponse.ts b/src/data/restObjects/RestTimelineFeedResponse.ts index 6118b9e36..d40410dcc 100644 --- a/src/data/restObjects/RestTimelineFeedResponse.ts +++ b/src/data/restObjects/RestTimelineFeedResponse.ts @@ -22,6 +22,7 @@ export interface RestTimelineFeedItem { cta_link: string | null cta_label: string | null url: string | null + user_registered_at: string | null time_zone: string | null mode: 'meeting' | 'online' | null post_address?: RestTimelineFeedAddress diff --git a/src/helpers/homeFeed.ts b/src/helpers/homeFeed.ts index 10df411f6..341d21d89 100644 --- a/src/helpers/homeFeed.ts +++ b/src/helpers/homeFeed.ts @@ -96,11 +96,11 @@ export const tranformFeedItemToProps = (feed: RestTimelineFeedItem): FeedCardPro title: feed.title, tag, image: feed.image ?? undefined, - isSubscribed: undefined, + isSubscribed: !!feed.user_registered_at, date: { start: feed.begin_at ? new Date(feed.begin_at) : new Date(feed.date), end: feed.finish_at ? new Date(feed.finish_at) : new Date(feed.date), - timeZone: feed.time_zone, + timeZone: feed.time_zone ?? undefined, }, location: feed.mode === 'online' ? undefined : location, isOnline: feed.mode === 'online', diff --git a/src/hooks/useEvents/useEvents.ts b/src/hooks/useEvents/useEvents.ts index 1409de4a4..5d80b3084 100644 --- a/src/hooks/useEvents/useEvents.ts +++ b/src/hooks/useEvents/useEvents.ts @@ -16,10 +16,10 @@ type FetchShortEventsOptions = { } const fetchEventList = async (pageParam: number, opts: FetchShortEventsOptions) => - await ApiService.getInstance().getEvents({ page: pageParam, zipCode: opts.postalCode, filters: opts.filters }) + await ApiService.getInstance().getEvents({ page: pageParam, zipCode: opts.postalCode, filters: opts.filters, orderByBeginAt: true }) const fetchEventPublicList = async (pageParam: number, opts: FetchShortEventsOptions) => { - return await ApiService.getInstance().getPublicEvents({ page: pageParam, filters: opts.filters, zoneCode: opts.zoneCode }) + return await ApiService.getInstance().getPublicEvents({ page: pageParam, filters: opts.filters, zoneCode: opts.zoneCode, orderByBeginAt: true }) } export const useSuspensePaginatedEvents = (opts: { filters?: EventFilters; postalCode?: string; zoneCode?: string }) => { diff --git a/src/screens/actions/ActionsScreen.tsx b/src/screens/actions/ActionsScreen.tsx index 65aa4c967..1280c74eb 100644 --- a/src/screens/actions/ActionsScreen.tsx +++ b/src/screens/actions/ActionsScreen.tsx @@ -21,7 +21,7 @@ const ActionsScreen = () => { data={actions} renderItem={renderItem} keyExtractor={(item) => item.id.toString()} - ListHeaderComponent={{i18n.t('actions.title')}} + // ListHeaderComponent={{i18n.t('actions.title')}} contentContainerStyle={styles.contentContainer} /> ) diff --git a/src/screens/buildingDetail/BuildingLayoutView.tsx b/src/screens/buildingDetail/BuildingLayoutView.tsx index 01394d594..9544d2dac 100644 --- a/src/screens/buildingDetail/BuildingLayoutView.tsx +++ b/src/screens/buildingDetail/BuildingLayoutView.tsx @@ -161,7 +161,7 @@ const SheetLeaflet = ({ open, onChange, onOpenChange }: { open: boolean; onChang inputMode="decimal" keyboardType="number-pad" width="100%" - label="Nombre de track distribué" + label="Nombre de documents distribué" selectTextOnFocus placeholder={'0'} value={value.toString()} diff --git a/src/screens/editPersonalInformation/PersonalInformations.tsx b/src/screens/editPersonalInformation/PersonalInformations.tsx index 4eb578028..d8acca5c9 100644 --- a/src/screens/editPersonalInformation/PersonalInformations.tsx +++ b/src/screens/editPersonalInformation/PersonalInformations.tsx @@ -20,6 +20,8 @@ import { RestUpdateProfileRequest } from '@/data/restObjects/RestUpdateProfileRe import { useDeleteProfil, useGetDetailProfil, useMutationUpdateProfil } from '@/hooks/useProfil' import { AddressFormatter } from '@/utils/AddressFormatter' import { format } from 'date-fns' +import { nativeApplicationVersion, nativeBuildVersion } from 'expo-application' +import Constants from 'expo-constants' import * as WebBrowser from 'expo-web-browser' import { Formik, FormikProps } from 'formik' import { isWeb, ScrollView, Spinner, Stack, useMedia, View, YStack } from 'tamagui' @@ -338,6 +340,9 @@ const EditInformations = () => { + + Version: v{Constants.expoConfig?.version ?? '0.0.0'} [{isWeb ? '???' : nativeBuildVersion} - {clientEnv.ENVIRONMENT}] + {isWeb && ( diff --git a/src/screens/events/EventFeedList.tsx b/src/screens/events/EventFeedList.tsx index 88e892735..302f9918a 100644 --- a/src/screens/events/EventFeedList.tsx +++ b/src/screens/events/EventFeedList.tsx @@ -1,10 +1,11 @@ -import { memo, useEffect, useMemo, useRef } from 'react' -import { FlatList, Platform } from 'react-native' +import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { Platform, SectionList } from 'react-native' import DialogAuth from '@/components/AuthDialog' +import Text from '@/components/base/Text' import { EventCard, PartialEventCard } from '@/components/Cards/EventCard' import EmptyEvent from '@/components/EmptyStates/EmptyEvent/EmptyEvent' import { bottomSheetFilterStates } from '@/components/EventFilterForm/BottomSheetFilters' -import EventFilterForm, { EventFilters, eventFiltersState, Controller as FilterController, FiltersState } from '@/components/EventFilterForm/EventFilterForm' +import EventFilterForm, { eventFiltersState, Controller as FilterController, FiltersState } from '@/components/EventFilterForm/EventFilterForm' import SearchBox from '@/components/EventFilterForm/SearchBox' import PageLayout from '@/components/layouts/PageLayout/PageLayout' import AuthFallbackWrapper from '@/components/Skeleton/AuthFallbackWrapper' @@ -12,26 +13,50 @@ import { useSession } from '@/ctx/SessionProvider' import { isFullEvent, isPartialEvent, RestEvent } from '@/data/restObjects/RestEvents' import { mapFullProps, mapPartialProps } from '@/helpers/eventsFeed' import { useSuspensePaginatedEvents } from '@/hooks/useEvents' +import { useScrollToTop } from '@react-navigation/native' +import { ChevronDown, Filter } from '@tamagui/lucide-icons' import { router } from 'expo-router' -import { getToken, Spinner, useMedia, YStack } from 'tamagui' +import { getToken, Spinner, useMedia, XStack, YStack } from 'tamagui' import { useDebounce } from 'use-debounce' const MemoizedEventCard = memo(EventCard) as typeof EventCard const MemoizedPartialEventCard = memo(PartialEventCard) as typeof PartialEventCard -const SmallHeaderList = (props: { listRef: React.RefObject }) => { +const splitEvents = (events: RestEvent[]) => { + let incomming: RestEvent[] = [] + let past: RestEvent[] = [] + events.forEach((event) => { + if (new Date(event.begin_at) < new Date()) { + past.push(event) + } else { + incomming.push(event) + } + }) + if (incomming.length === 0 && past.length === 0) { + return [] + } + + return [ + { title: 'À venir', data: incomming, index: 0 }, + { title: 'Évènements passées', data: past, index: 1 }, + ] +} + +const SmallHeaderList = (props: { listRef: React.RefObject> }) => { const { setOpen, open } = bottomSheetFilterStates() + const sections = props.listRef.current?.props.sections + const data = Boolean(sections && sections.length > 0 && (sections[0].data.length > 0 || sections[1].data.length > 0)) useEffect(() => { - if (!open) { + if (!open && data) { setTimeout(() => { - props.listRef.current?.scrollToOffset({ offset: 0, animated: true }) + props.listRef.current?.scrollToLocation({ itemIndex: 0, sectionIndex: 0, animated: true, viewOffset: 100 }) }, 300) } }, [open]) const handleFocus = (searchInputRef: FiltersState['searchInputRef']) => () => { setOpen(true) - const data = Boolean(props.listRef.current?.props.data ? props.listRef.current?.props.data.length : 0) - if (data) props.listRef.current?.scrollToIndex({ index: 0, animated: true, viewOffset: 15 }) + + if (data) props.listRef.current?.scrollToLocation({ itemIndex: 0, sectionIndex: 0, animated: true, viewOffset: Platform.OS === 'android' ? -30 : -100 }) setTimeout(() => { searchInputRef.current?.focus() }, 0) @@ -41,14 +66,22 @@ const SmallHeaderList = (props: { listRef: React.RefObject }) => { {(p) => ( - + )} ) } -const HeaderList = (props: { listRef: React.RefObject }) => { +const HeaderList = (props: { listRef: React.RefObject }) => { const media = useMedia() if (media.md) { return @@ -84,10 +117,11 @@ const EventListCard = memo((args: { item: RestEvent; cb: Parameters { +const EventList = ({ activeTab }: { activeTab: 'events' | 'myEvents' }) => { const media = useMedia() const { user } = useSession() - const listRef = useRef(null) + const listRef = useRef(null) + useScrollToTop(listRef) const { value: _filters } = eventFiltersState() const [filters] = useDebounce(_filters, 300) @@ -101,8 +135,8 @@ const EventList = () => { } = useSuspensePaginatedEvents({ postalCode: user.data?.postal_code, filters: { - finishAfter: filters.showPast ? undefined : new Date(), searchText: filters.search, + subscribedOnly: activeTab === 'myEvents', }, }) @@ -119,7 +153,10 @@ const EventList = () => { [], ) - const feedData = paginatedFeed?.pages.map((page) => page.items).flat() ?? [] + const feedData = useMemo(() => { + if (!paginatedFeed) return [] + return splitEvents(paginatedFeed.pages.flatMap((page) => page.items)) + }, [paginatedFeed]) const loadMore = () => { if (hasNextPage) { @@ -128,27 +165,45 @@ const EventList = () => { } return ( - } + renderSectionHeader={({ section }) => { + return ( + + + {section.title} {section.index === 0 ? `(${section.data.length})` : ''} + + + + ) + }} ListEmptyComponent={ } keyboardDismissMode="on-drag" - ListHeaderComponent={media.lg ? : null} + ListHeaderComponent={ + media.lg ? ( + + + + ) : null + } keyExtractor={(item) => item.uuid} refreshing={isRefetching} onRefresh={() => refetch()} diff --git a/src/screens/home/feed/HomeFeedList.tsx b/src/screens/home/feed/HomeFeedList.tsx index c9e9df5e6..1d766c41a 100644 --- a/src/screens/home/feed/HomeFeedList.tsx +++ b/src/screens/home/feed/HomeFeedList.tsx @@ -1,15 +1,18 @@ -import { memo } from 'react' +import { memo, useRef } from 'react' import { FlatList } from 'react-native' import { FeedCard } from '@/components/Cards' import { useSession } from '@/ctx/SessionProvider' import { RestTimelineFeedItem } from '@/data/restObjects/RestTimelineFeedResponse' import { tranformFeedItemToProps } from '@/helpers/homeFeed' import { useGetPaginatedFeed } from '@/hooks/useFeed' +import { useScrollToTop } from '@react-navigation/native' import { getToken, Spinner, useMedia, YStack } from 'tamagui' +const FeedCardMemoized = memo(FeedCard) as typeof FeedCard + const TimelineFeedCard = memo((item: RestTimelineFeedItem) => { const props = tranformFeedItemToProps(item) - return + return }) const renderFeedItem = ({ item }: { item: RestTimelineFeedItem }) => { @@ -26,9 +29,12 @@ const HomeFeedList = () => { fetchNextPage() } } + const flatListRef = useRef>(null) + useScrollToTop(flatListRef) return ( = (props) => { if (Platform.OS === 'ios') { return ( - - - {props.children} - + + {props.children} ) } else { diff --git a/src/screens/tools/ResourcesList.tsx b/src/screens/tools/ResourcesList.tsx index d3f4152bf..f1071bd22 100644 --- a/src/screens/tools/ResourcesList.tsx +++ b/src/screens/tools/ResourcesList.tsx @@ -1,12 +1,15 @@ -import React from 'react' +import React, { useRef } from 'react' import { RefreshControl, ScrollView } from 'react-native' import { useTools } from '@/hooks/useTools' import CardTool from '@/screens/tools/components/CardTool' +import { useScrollToTop } from '@react-navigation/native' import { getToken, useMedia, View } from 'tamagui' const ResourcesList = () => { const media = useMedia() const { data, refetch, isRefetching } = useTools() + const ref = useRef(null) + useScrollToTop(ref) const tools = data.pages .map((_) => _.items) @@ -19,6 +22,7 @@ const ResourcesList = () => { return (