158 lines
5.6 KiB
TypeScript
158 lines
5.6 KiB
TypeScript
/* eslint-disable @typescript-eslint/ban-types */
|
|
import { NativeStackScreenProps, createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
import {
|
|
NavigationProp,
|
|
ParamListBase,
|
|
useNavigation as navigationHook,
|
|
} from '@react-navigation/native';
|
|
import React, { useEffect } from 'react';
|
|
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
|
|
import { RootState, useSelector } from './state/Store';
|
|
import { useDispatch } from 'react-redux';
|
|
import { Translate, translate } from './i18n/i18n';
|
|
import SongLobbyView from './views/SongLobbyView';
|
|
import AuthenticationView from './views/AuthenticationView';
|
|
import StartPageView from './views/StartPageView';
|
|
import HomeView from './views/HomeView';
|
|
import SearchView from './views/SearchView';
|
|
import SetttingsNavigator from './views/settings/SettingsView';
|
|
import { useQuery } from 'react-query';
|
|
import API, { APIError } from './API';
|
|
import PlayView from './views/PlayView';
|
|
import ScoreView from './views/ScoreView';
|
|
import { LoadingView } from './components/Loading';
|
|
import ProfileView from './views/ProfileView';
|
|
import useColorScheme from './hooks/colorScheme';
|
|
import ArtistDetailsView from './views/ArtistDetailsView';
|
|
import { Button, Center, VStack } from 'native-base';
|
|
import { unsetAccessToken } from './state/UserSlice';
|
|
import TextButton from './components/TextButton';
|
|
|
|
const protectedRoutes = () =>
|
|
({
|
|
Home: { component: HomeView, options: { title: translate('welcome'), headerLeft: null } },
|
|
Play: { component: PlayView, options: { title: translate('play') } },
|
|
Settings: { component: SetttingsNavigator, options: { title: 'Settings' } },
|
|
Song: { component: SongLobbyView, options: { title: translate('play') } },
|
|
Artist: { component: ArtistDetailsView, options: { title: translate('artistFilter') } },
|
|
Score: { component: ScoreView, options: { title: translate('score'), headerLeft: null } },
|
|
Search: { component: SearchView, options: { title: translate('search') } },
|
|
User: { component: ProfileView, options: { title: translate('user') } },
|
|
} as const);
|
|
|
|
const publicRoutes = () =>
|
|
({
|
|
Start: { component: StartPageView, options: { title: 'Chromacase', headerShown: false } },
|
|
Login: { component: AuthenticationView, options: { title: translate('signInBtn') } },
|
|
Oops: { component: ProfileErrorView, options: { title: 'Oops', headerShown: false } },
|
|
} as const);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
type Route<Props = any> = {
|
|
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element);
|
|
options: object;
|
|
};
|
|
|
|
type OmitOrUndefined<T, K extends string> = T extends undefined ? T : Omit<T, K>;
|
|
|
|
type RouteParams<Routes extends Record<string, Route>> = {
|
|
[RouteName in keyof Routes]: OmitOrUndefined<
|
|
Parameters<Routes[RouteName]['component']>[0],
|
|
keyof NativeStackScreenProps<{}>
|
|
>;
|
|
};
|
|
|
|
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
|
|
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
|
|
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
|
|
|
|
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never }>();
|
|
|
|
const RouteToScreen =
|
|
<T extends {}>(component: Route<T>['component']) =>
|
|
// eslint-disable-next-line react/display-name
|
|
(props: NativeStackScreenProps<T & ParamListBase>) =>
|
|
(
|
|
<>
|
|
{component({ ...props.route.params, route: props.route } as Parameters<
|
|
Route<T>['component']
|
|
>[0])}
|
|
</>
|
|
);
|
|
|
|
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
|
|
Object.entries(routes).map(([name, route], routeIndex) => (
|
|
<Stack.Screen
|
|
key={'route-' + routeIndex}
|
|
name={name as keyof AppRouteParams}
|
|
options={route.options}
|
|
component={RouteToScreen(route.component)}
|
|
/>
|
|
));
|
|
|
|
const ProfileErrorView = (props: { onTryAgain: () => void }) => {
|
|
const dispatch = useDispatch();
|
|
return (
|
|
<Center style={{ flexGrow: 1 }}>
|
|
<VStack space={3}>
|
|
<Translate translationKey="userProfileFetchError" />
|
|
<Button onPress={props.onTryAgain}>
|
|
<Translate translationKey="tryAgain" />
|
|
</Button>
|
|
<TextButton
|
|
onPress={() => dispatch(unsetAccessToken())}
|
|
colorScheme="error"
|
|
variant="outline"
|
|
translate={{ translationKey: 'signOutBtn' }}
|
|
/>
|
|
</VStack>
|
|
</Center>
|
|
);
|
|
};
|
|
|
|
export const Router = () => {
|
|
const dispatch = useDispatch();
|
|
const accessToken = useSelector((state: RootState) => state.user.accessToken);
|
|
const userProfile = useQuery(['user', 'me', accessToken], () => API.getUserInfo(), {
|
|
retry: 1,
|
|
refetchOnWindowFocus: false,
|
|
onError: (err) => {
|
|
if (err instanceof APIError && err.status === 401) {
|
|
dispatch(unsetAccessToken());
|
|
}
|
|
},
|
|
});
|
|
const colorScheme = useColorScheme();
|
|
|
|
useEffect(() => {
|
|
if (accessToken) {
|
|
userProfile.refetch();
|
|
}
|
|
}, [accessToken]);
|
|
|
|
return (
|
|
<NavigationContainer theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}>
|
|
<Stack.Navigator>
|
|
{userProfile.isError && accessToken && !userProfile.isLoading ? (
|
|
<Stack.Screen
|
|
name="Oops"
|
|
component={RouteToScreen(() => (
|
|
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
|
|
))}
|
|
/>
|
|
) : userProfile.isLoading && !userProfile.data ? (
|
|
<Stack.Screen name="Loading" component={RouteToScreen(LoadingView)} />
|
|
) : (
|
|
routesToScreens(
|
|
userProfile.isSuccess && accessToken ? protectedRoutes() : publicRoutes()
|
|
)
|
|
)}
|
|
</Stack.Navigator>
|
|
</NavigationContainer>
|
|
);
|
|
};
|
|
|
|
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
|
|
|
|
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();
|