Merge pull request #190 from Chroma-Case/front/typesafe-navigator

Front: Navigation: Use actual routes to build a typed navigator
This commit is contained in:
Arthur Jamet
2023-04-14 14:27:03 +01:00
committed by GitHub
10 changed files with 62 additions and 33 deletions

View File

@@ -1,4 +1,5 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { NativeStackScreenProps, createNativeStackNavigator } from '@react-navigation/native-stack';
import { NavigationProp, useNavigation as navigationHook } from "@react-navigation/native";
import React from 'react';
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { RootState, useSelector } from './state/Store';
@@ -17,21 +18,48 @@ import LoadingComponent from './components/Loading';
import ProfileView from './views/ProfileView';
import useColorScheme from './hooks/colorScheme';
const Stack = createNativeStackNavigator();
const protectedRoutes = () => ({
Home: { component: HomeView, options: { title: translate('welcome') } },
Settings: { component: SetttingsNavigator, options: { title: 'Settings' } },
Song: { component: SongLobbyView, options: { title: translate('play') } },
Play: { component: PlayView, options: { title: translate('play') } },
Score: { component: ScoreView, options: { title: translate('score') } },
Search: { component: SearchView, options: { title: translate('search') } },
User: { component: ProfileView, options: { title: translate('user') } },
}) as const;
export const protectedRoutes = <>
<Stack.Screen name="Home" component={HomeView} options={{ title: translate('welcome') }} />
<Stack.Screen name="Settings" component={SetttingsNavigator} options={{ title: 'Settings' }} />
<Stack.Screen name="Song" component={SongLobbyView} options={{ title: translate('play') }} />
<Stack.Screen name="Play" component={PlayView} options={{ title: translate('play') }} />
<Stack.Screen name="Score" component={ScoreView} options={{ title: translate('score') }} />
<Stack.Screen name="Search" component={SearchView} options={{ title: translate('search') }} />
<Stack.Screen name="User" component={ProfileView} options={{ title: translate('user') }} />
</>;
const publicRoutes = () => ({
Login: { component: AuthenticationView, options: { title: translate('signInBtn') } },
}) as const;
export const publicRoutes = <React.Fragment>
<Stack.Screen name="Login" component={AuthenticationView} options={{ title: translate('signInBtn')}} />
</React.Fragment>;
type Route<Args extends any[] = any[]> = {
component: (...args: Args) => JSX.Element,
options: any
}
type RouteParams<Routes extends Record<string, Route>> = {
[RouteName in keyof Routes]: Parameters<Routes[RouteName]['component']>[0];
}
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never }>();
const RouteToScreen = (component: Route['component']) => (props: NativeStackScreenProps<AppRouteParams>) =>
<>
{component(props.route.params)}
</>
const routesToScreens = (routes: Record<keyof AppRouteParams, Route>) => Object.entries(routes)
.map(([name, route]) => (
<Stack.Screen
name={name as keyof AppRouteParams}
options={route.options}
component={RouteToScreen(route.component)}
/>
))
export const Router = () => {
const accessToken = useSelector((state: RootState) => state.user.accessToken);
@@ -53,11 +81,13 @@ export const Router = () => {
<LoadingComponent/>
</Center>
}/>
: userProfile.isSuccess && accessToken
? protectedRoutes
: publicRoutes
: routesToScreens(userProfile.isSuccess && accessToken
? protectedRoutes()
: publicRoutes())
}
</Stack.Navigator>
</NavigationContainer>
);
}
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();

View File

@@ -1,4 +1,4 @@
import { useNavigation } from "@react-navigation/core";
import { useNavigation } from "../Navigation";
import { HStack, VStack, Text, Progress } from "native-base";
import { translate } from "../i18n/i18n";
import Card from './Card';

View File

@@ -1,7 +1,7 @@
import React from "react";
import { translate } from "../i18n/i18n";
import { Box, useBreakpointValue, Text, VStack, Progress, Stack, AspectRatio } from 'native-base';
import { useNavigation } from "@react-navigation/native";
import { useNavigation } from "../Navigation";
import { Pressable, Image } from "native-base";
import Card from "../components/Card";

View File

@@ -1,7 +1,7 @@
import React from "react";
import Card, { CardBorderRadius } from './Card';
import { VStack, Text, Image, Pressable } from 'native-base';
import { useNavigation } from "@react-navigation/core";
import { useNavigation } from "../Navigation";
type SongCardProps = {
albumCover: string;
songTitle: string;

View File

@@ -3,7 +3,8 @@ import { useQueries, useQuery } from "react-query";
import API from "../API";
import LoadingComponent from "../components/Loading";
import { Center, Box, ScrollView, Flex, useBreakpointValue, Stack, Heading, Container, VStack, HStack } from 'native-base';
import { useNavigation } from "@react-navigation/native";
import { useNavigation } from "../Navigation";
import SongCardGrid from '../components/SongCardGrid';
import CompetenciesTable from '../components/CompetenciesTable'
import ProgressBar from "../components/ProgressBar";

View File

@@ -4,7 +4,8 @@ import * as ScreenOrientation from 'expo-screen-orientation';
import { Box, Center, Column, Progress, Text, Row, View, useToast, Icon } from 'native-base';
import IconButton from '../components/IconButton';
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useNavigation } from '@react-navigation/native';
import { useNavigation } from "../Navigation";
import { useQuery, useQueryClient } from 'react-query';
import API from '../API';
import LoadingComponent from '../components/Loading';
@@ -32,8 +33,7 @@ if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') {
}
}
const PlayView = () => {
const songId = 1;
const PlayView = ({ songId }: PlayViewProps) => {
const navigation = useNavigation();
const queryClient = useQueryClient();
const song = useQuery(['song', songId], () => API.getSong(songId));

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Dimensions, View } from 'react-native';
import { Box, Image, Heading, HStack, Card, Button, Spacer, Text } from 'native-base';
import Translate from '../components/Translate';
import { useNavigation } from '@react-navigation/native';
import { useNavigation } from "../Navigation";
import TextButton from '../components/TextButton';
const UserMedals = () => {

View File

@@ -1,14 +1,14 @@
import { Card, Column, Image, Row, Text, useTheme, ScrollView, Center, VStack } from "native-base"
import Translate from "../components/Translate";
import SongCardGrid from "../components/SongCardGrid";
import { useNavigation } from "@react-navigation/native";
import { useNavigation } from "../Navigation";
import { CardBorderRadius } from "../components/Card";
import TextButton from "../components/TextButton";
import API from '../API';
import { useQuery } from "react-query";
import LoadingComponent from "../components/Loading";
const ScoreView = (/*{ songId }, { songId: number }*/) => {
const ScoreView = ({ songId }: { songId: number }) => {
const theme = useTheme();
const navigation = useNavigation();
// const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Box } from "native-base";
import { useNavigation } from "@react-navigation/native";
import { useNavigation } from "../Navigation";
import SearchBarSuggestions from "../components/SearchBarSuggestions";
import { useQueries, useQuery } from "react-query";
import { SuggestionType } from "../components/SearchBar";

View File

@@ -1,4 +1,3 @@
import { useNavigation, useRoute } from "@react-navigation/native";
import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon } from "native-base";
import { useQuery } from 'react-query';
import LoadingComponent from "../components/Loading";
@@ -8,16 +7,15 @@ import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
import API from "../API";
import TextButton from "../components/TextButton";
import { useNavigation } from "../Navigation";
interface SongLobbyProps {
// The unique identifier to find a song
songId: number;
}
const SongLobbyView = () => {
const route = useRoute();
const SongLobbyView = (props: SongLobbyProps) => {
const navigation = useNavigation();
const props: SongLobbyProps = route.params as any;
const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));
const chaptersQuery = useQuery(['song', props.songId, 'chapters'], () => API.getSongChapters(props.songId));
const scoresQuery = useQuery(['song', props.songId, 'scores'], () => API.getSongHistory(props.songId));
@@ -47,7 +45,7 @@ const SongLobbyView = () => {
/>
</Text>
<TextButton translate={{ translationKey: 'playBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data?.id })}
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
/>
</Box>