Front: Fix API calls with JWT Token

This commit is contained in:
Arthi-chaud
2022-11-13 15:48:52 +00:00
parent 4ecd556918
commit 889d07cfe5
22 changed files with 368 additions and 248 deletions
+1
View File
@@ -4,3 +4,4 @@ POSTGRES_NAME=
POSTGRES_HOST=
DATABASE_URL=
JWT_SECRET=
API_URL=
+1
View File
@@ -18,6 +18,7 @@ async function bootstrap() {
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe());
app.enableCors();
await app.listen(3000);
}
bootstrap();
+3 -1
View File
@@ -21,6 +21,8 @@ services:
front:
build: ./front
ports:
- "8080:80"
- "80:80"
depends_on:
- "back"
env_file:
- .env
+142 -80
View File
@@ -1,3 +1,4 @@
import Artist from "./models/Artist";
import AuthToken from "./models/AuthToken";
import Chapter from "./models/Chapter";
import Lesson from "./models/Lesson";
@@ -5,51 +6,88 @@ import LessonHistory from "./models/LessonHistory";
import Song from "./models/Song";
import SongHistory from "./models/SongHistory";
import User from "./models/User";
import { translate } from "./i18n/i18n";
import Constants from 'expo-constants';
import store from "./state/Store";
const delay = (seconds: number) => new Promise(resolve => setTimeout(resolve, seconds * 1000));
type AuthenticationInput = { username: string, password: string };
type RegistrationInput = AuthenticationInput & { email: string };
export type AccessToken = string;
declare type AuthenticationInput = { email: string, password: string };
type FetchParams = {
route: string;
body?: Object;
method?: 'GET' | 'POST' | 'DELETE'
}
const dummyIllustration = "https://i.discogs.com/syRCX8NaLwK2SMk8X6TVU_DWc8RRqE4b-tebAQ6kVH4/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTgyNTQz/OC0xNjE3ODE0NDI2/LTU1MjUuanBlZw.jpeg";
export default class API {
private static async fetch(params: FetchParams) {
const jwtToken = store.getState().user.accessToken;
const header = {
'Content-Type': 'application/json'
}
const response = await fetch(`${Constants.manifest?.extra?.apiUrl}${params.route}`, {
headers: jwtToken && { ...header, 'Authorization': `Bearer ${jwtToken}` } || header,
body: JSON.stringify(params.body),
method: params.method ?? 'GET'
});
const jsonResponse = await response.json().catch(() => {
throw new Error("Error while parsing Server's response");
});
if (!response.ok) {
throw new Error(jsonResponse.error ?? response.statusText)
}
return jsonResponse;
}
public static async authenticate(authenticationInput: AuthenticationInput): Promise<AccessToken> {
return API.fetch({
route: '/auth/login',
body: authenticationInput,
method: 'POST'
}).then((responseBody) => responseBody.access_token)
}
/**
* Create a new user profile, with an email and a password
* @param registrationInput the credentials to create a new profile
* @returns A Promise. On success, will be resolved into an instance of the API wrapper
*/
public static async createAccount(registrationInput: RegistrationInput): Promise<AccessToken> {
await API.fetch({
route: '/auth/register',
body: registrationInput,
method: 'POST'
});
return API.authenticate(registrationInput);
}
/***
* Retrieve information of the currently authentified user
*/
static async getUserInfo(): Promise<User> {
public static async getUserInfo(): Promise<User> {
return API.fetch({
route: '/auth/me'
});
}
public static async getUserSkills() {
return {
name: "User",
email: "user@chromacase.com",
xp: 2345,
premium: false,
metrics: {},
settings: {},
id: 1
pedalsCompetency: Math.random() * 100,
rightHandCompetency: Math.random() * 100,
leftHandCompetency: Math.random() * 100,
accuracyCompetency: Math.random() * 100,
arpegeCompetency: Math.random() * 100,
chordsCompetency: Math.random() * 100,
}
}
/**
* Logs the user in, with an email and a password
* @param _credentials the credentials to get an authentication token
* @returns an authentication token, that must be used for authentified requests
*/
static async login(_credentials: AuthenticationInput): Promise<AuthToken> {
return "12345";
}
/**
* Create a new user profile, with an email and a password
* @param _credentials the credentials to create a new profile
* @returns an empty promise. On error, the promise will not be resolved
*/
static async register(_credentials: AuthenticationInput): Promise<void> {
return;
}
/**
* Authentify a new user through Google
*/
static async authWithGoogle(): Promise<AuthToken> {
public static async authWithGoogle(): Promise<AuthToken> {
//TODO
return "11111";
}
@@ -57,22 +95,27 @@ export default class API {
* Retrive a song
* @param songId the id to find the song
*/
static async getSong(songId: number): Promise<Song> {
return delay(1).then(() => ({
title: "Song",
description: "A very very very very very very very very very very very very very very very very very very very very very very very very good song",
album: "Album",
metrics: {},
id: songId
}));
public static async getSong(songId: number): Promise<Song> {
return API.fetch({
route: `/song/${songId}`
});
}
/**
* Retrive an artist
*/
public static async getArtist(artistId: number): Promise<Artist> {
return API.fetch({
route: `/artist/${artistId}`
});
}
/**
* Retrive a song's chapters
* @param songId the id to find the song
*/
static async getSongChapters(songId: number): Promise<Chapter[]> {
public static async getSongChapters(songId: number): Promise<Chapter[]> {
return [1, 2, 3, 4, 5].map((value) => ({
start: 100 * (value - 1),
end: 100 * value,
@@ -89,7 +132,7 @@ export default class API {
* Retrieve a song's play history
* @param songId the id to find the song
*/
static async getSongHistory(songId: number): Promise<SongHistory[]> {
public static async getSongHistory(songId: number): Promise<SongHistory[]> {
return [6, 1, 2, 3, 4, 5].map((value) => ({
songId: songId,
userId: 1,
@@ -101,21 +144,23 @@ export default class API {
* Search a song by its name
* @param query the string used to find the songs
*/
static async searchSongs(query: string): Promise<Song[]> {
return [{
title: "Song",
description: "A song",
album: "Album",
metrics: {},
id: 1
}];
public static async searchSongs(query: string): Promise<Song[]> {
return Array.of(4).map((i) => ({
id: i,
name: `Searched Song ${i}`,
artistId: i,
genreId: i,
albumId: i,
cover: dummyIllustration,
metrics: {}
}));
}
/**
* Retrieve a lesson
* @param lessonId the id to find the lesson
*/
static async getLesson(lessonId: number): Promise<Lesson> {
public static async getLesson(lessonId: number): Promise<Lesson> {
return {
title: "Song",
description: "A song",
@@ -125,43 +170,60 @@ export default class API {
};
}
/**
* Retrieve the authenticated user's search history
* @param lessonId the id to find the lesson
*/
public static async getSearchHistory(): Promise<Song[]> {
return Array.of(4).map((i) => ({
id: i,
name: `Song in history ${i}`,
artistId: i,
genreId: i,
albumId: i,
cover: dummyIllustration,
metrics: {}
}));
}
/**
* Retrieve the authenticated user's recommendations
*/
public static async getUserRecommendations(): Promise<Song[]> {
return Array.of(4).map((i) => ({
id: i,
name: `Recommended Song ${i}`,
artistId: i,
genreId: i,
albumId: i,
cover: dummyIllustration,
metrics: {}
}));
}
/**
* Retrieve the authenticated user's play history
*/
public static async getUserPlayHistory(): Promise<Song[]> {
return Array.of(4).map((i) => ({
id: i,
name: `played Song ${i}`,
artistId: i,
genreId: i,
albumId: i,
cover: dummyIllustration,
metrics: {}
}));
}
/**
* Retrieve a lesson's history
* @param lessonId the id to find the lesson
*/
static async getLessonHistory(lessonId: number): Promise<LessonHistory[]> {
public static async getLessonHistory(lessonId: number): Promise<LessonHistory[]> {
return [{
lessonId,
userId: 1
}];
}
/**
* Get the login information status
*
*/
static async checkSigninCredentials(username: string, password: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
if (username === "katerina" && password === "1234") {
return resolve("token signin");
}
return reject(translate("invalidCredentials"));
}, 1000);
});
};
/**
* Get the register information status
*/
static async checkSignupCredentials(username: string, password: string, email: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
setTimeout(() => {
if (username === "bluub") {
return reject(translate("usernameTaken"));
}
return resolve("token signup");
}, 1000);
});
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ export const publicRoutes = <React.Fragment>
</React.Fragment>;
export const Router = () => {
const isAuthentified = useSelector((state) => state.user.token !== undefined)
const isAuthentified = useSelector((state) => state.user.accessToken !== undefined)
return (
<NavigationContainer>
<Stack.Navigator>
+40
View File
@@ -0,0 +1,40 @@
module.exports = {
"name": "Chromacase",
"slug": "Chromacase",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/cover.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF",
"package": "com.chromacase.chromacase",
"versionCode": 1
},
"package": "build.apk"
},
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
apiUrl: process.env.API_URL,
"eas": {
"projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2"
}
}
}
-41
View File
@@ -1,41 +0,0 @@
{
"expo": {
"name": "Chromacase",
"slug": "Chromacase",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#FFFFFF",
"package": "com.chromacase.chromacase",
"versionCode": 1
},
"package": "build.apk"
},
"web": {
"favicon": "./assets/favicon.png"
},
"extra": {
"eas": {
"projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2"
}
}
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import {
Square,
} from "native-base";
import React from "react";
import { Ionicons, FontAwesome } from "@expo/vector-icons";
import { Ionicons } from "@expo/vector-icons";
export enum SuggestionType {
TEXT,
+2 -2
View File
@@ -1,5 +1,5 @@
import React from "react";
import SearchBar from "../components/SearchBar";
import SearchBar, { IllustratedSuggestionProps } from "../components/SearchBar";
import { SuggestionList, SuggestionType } from "../components/SearchBar";
interface SearchBarSuggestionsProps {
onTextSubmit: (text: string) => void;
@@ -15,7 +15,7 @@ const filterSuggestions = (text: string, suggestions: SuggestionList) => {
case SuggestionType.ILLUSTRATED:
return (
suggestion.data.text.toLowerCase().includes(text.toLowerCase()) ||
suggestion.data.subtext.toLowerCase().includes(text.toLowerCase())
(suggestion.data as IllustratedSuggestionProps).subtext.toLowerCase().includes(text.toLowerCase())
);
}
});
+1 -1
View File
@@ -22,7 +22,7 @@ const SongCard = (props: SongCardProps) => {
>
<Image
style={{ zIndex: 0, aspectRatio: 1, margin: 5, borderRadius: CardBorderRadius}}
source={{ uri: "https://i.discogs.com/yHqu3pnLgJq-cVpYNVYu6mE-fbzIrmIRxc6vES5Oi48/rs:fit/g:sm/q:90/h:556/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTE2NjQ2/ODUwLTE2MDkwNDU5/NzQtNTkxOS5qcGVn.jpeg" }}
source={{ uri: albumCover }}
alt={[props.songTitle, props.artistName].join('-')}
/>
<VStack padding={3}>
-1
View File
@@ -112,7 +112,6 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
toast.show({ description: resp, colorScheme: 'secondary' })
} catch (e) {
toast.show({ description: e as string, colorScheme: 'red', avoidKeyboard: true })
} finally {
setSubmittingForm(false);
}
}}
+1 -1
View File
@@ -52,7 +52,7 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
.min(4, translate("passwordTooShort"))
.max(100, translate("passwordTooLong"))
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$-_%\^&\*])(?=.{8,})/,
translate(
"Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character"
)
+5 -3
View File
@@ -2,9 +2,11 @@ import Metrics from "./Metrics";
import Model from "./Model";
interface Song extends Model {
title: string;
description: string;
album: string;
name: string
artistId: number | null
albumId: number | null
genreId: number | null;
cover: string;
metrics: Metrics;
}
+3
View File
@@ -48,8 +48,11 @@
"devDependencies": {
"@babel/core": "^7.12.9",
"@testing-library/react-native": "^11.0.0",
"@types/node": "^18.11.8",
"@types/react": "^18.0.18",
"@types/react-native": "^0.69.6",
"@types/react-navigation": "^3.4.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"react-test-renderer": "17.0.2",
"typescript": "~4.3.5"
},
+7 -7
View File
@@ -1,19 +1,19 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import API from '../API';
import { AccessToken } from '../API';
export const userSlice = createSlice({
name: 'user',
initialState: {
apiAccess: undefined as API | undefined
accessToken: undefined as AccessToken | undefined
},
reducers: {
setAPIAccess: (state, action: PayloadAction<API>) => {
state.apiAccess = action.payload;
setAccessToken: (state, action: PayloadAction<AccessToken>) => {
state.accessToken = action.payload;
},
unsetAPIAccess: (state) => {
state.apiAccess = undefined;
unsetAccessToken: (state) => {
state.accessToken = undefined;
},
},
});
export const { setAPIAccess, unsetAPIAccess } = userSlice.actions;
export const { setAccessToken, unsetAccessToken } = userSlice.actions;
export default userSlice.reducer;
+1 -1
View File
@@ -32,7 +32,7 @@
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": ["react-native", "jest"], /* Specify type package names to be included without being referenced in a source file. */
"types": ["react-native", "jest", "node"], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
+13 -17
View File
@@ -2,28 +2,28 @@ import React from "react";
import { useDispatch } from '../state/Store';
import { translate } from "../i18n/i18n";
import API from "../API";
import { setUserToken } from "../state/UserSlice";
import { setAccessToken } from "../state/UserSlice";
import { Center, Button, Text } from 'native-base';
import SigninForm from "../components/forms/signinform";
import SignupForm from "../components/forms/signupform";
const hanldeSignin = async (username: string, password: string, tokenSetter: (token: string) => void): Promise<string> => {
const hanldeSignin = async (username: string, password: string, apiSetter: (accessToken: string) => void): Promise<string> => {
try {
const response = await API.checkSigninCredentials(username, password);
tokenSetter(response);
const apiAccess = await API.authenticate({ username, password });
apiSetter(apiAccess);
return translate("loggedIn");
} catch (error) {
return error as string;
return "Username of password incorrect";
}
};
const handleSignup = async (username: string, password: string, email: string, tokenSetter: (t: string) => void): Promise<string> => {
const handleSignup = async (username: string, password: string, email: string, apiSetter: (accessToken: string) => void): Promise<string> => {
try {
const response = await API.checkSignupCredentials(username, password, email);
tokenSetter(response);
const apiAccess = await API.createAccount({ username, password, email });
apiSetter(apiAccess);
return translate("loggedIn");
} catch (error) {
return error as string;
return "User already exists";
}
};
@@ -33,14 +33,10 @@ const AuthenticationView = () => {
return (
<Center style={{ flex: 1 }}>
<Text>{translate('welcome')}</Text>
{mode === "signin" ? (<>
<Text fontWeight='thin'>username, password:</Text>
<Text fontWeight='thin'>katerina, 1234</Text>
<SigninForm onSubmit={(username, password) => hanldeSignin(username, password, (token) => dispatch(setUserToken(token)))} />
</>) : (
<SignupForm onSubmit={(username, password, email) => handleSignup(username, password, email, (token) => dispatch(setUserToken(token)))} />
)}
{mode === "signin"
? <SigninForm onSubmit={(username, password) => hanldeSignin(username, password, (accessToken) => dispatch(setAccessToken(accessToken)))} />
: <SignupForm onSubmit={(username, password, email) => handleSignup(username, password, email, (accessToken) => dispatch(setAccessToken(accessToken)))} />
}
{ mode ==="signin" && <Button variant="outline" marginTop={5} colorScheme="error" >{translate("forgottenPassword")}</Button> }
<Button variant='outline' marginTop={5} colorScheme='primary' onPress={() => setMode(mode === "signin" ? "signup" : "signin")}>
<Text>{translate(mode === "signin" ? "signUp" : "signIn")}</Text>
+36 -32
View File
@@ -1,8 +1,8 @@
import React from "react";
import { useQuery } from "react-query";
import React, { useEffect, useState } from "react";
import { useQueries, useQuery } from "react-query";
import API from "../API";
import LoadingComponent from "../components/Loading";
import { Box, ScrollView, Flex, useBreakpointValue, Text, VStack, Progress, Button, useTheme, Heading, Divider } from 'native-base';
import { Box, ScrollView, Flex, useBreakpointValue, Text, VStack, Progress, Button, useTheme, Heading } from 'native-base';
import { useNavigation } from "@react-navigation/native";
import SongCardGrid from '../components/SongCardGrid';
import CompetenciesTable from '../components/CompetenciesTable'
@@ -31,15 +31,20 @@ const HomeView = () => {
const screenSize = useBreakpointValue({ base: 'small', md: "big"});
const flexDirection = useBreakpointValue({ base: 'column', xl: "row"});
const userQuery = useQuery(['user'], () => API.getUserInfo());
if (!userQuery.data) {
const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory());
const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory());
const skillsQuery = useQuery(['skills'], () => API.getUserSkills());
const nextStepQuery = useQuery(['user', 'recommendations'], () => API.getUserRecommendations());
const artistsQueries = useQueries((playHistoryQuery.data?.concat(searchHistoryQuery.data ?? []).concat(nextStepQuery.data ?? []) ?? []).map((song) => (
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) }
)));
if (!userQuery.data || !skillsQuery.data || !searchHistoryQuery.data || !playHistoryQuery.data) {
return <Box style={{ flexGrow: 1, justifyContent: 'center' }}>
<LoadingComponent/>
</Box>
}
return <ScrollView>
<Box style={{ display: 'flex', padding: 30 }}>
<Box textAlign={ screenSize == 'small' ? 'center' : undefined } style={{ flexDirection, justifyContent: 'center', display: 'flex' }}>
<Text fontSize="xl" flex={screenSize == 'small' ? 1 : 2}>{`${translate('welcome')} ${userQuery.data.name}!`} </Text>
<Box flex={1}>
@@ -51,26 +56,21 @@ const HomeView = () => {
<Box flex={2}>
<SongCardGrid
heading={translate('goNextStep')}
songs={[ ...Array(4).keys() ].map(() => ({
albumCover: "",
songTitle: "Song",
artistName: "Artist",
songId: 1
}))}
songs={nextStepQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
<Flex style={{ flexDirection }}>
<Box flex={1} paddingY={5}>
<Heading>{translate('mySkillsToImprove')}</Heading>
<Box padding={5}>
<CompetenciesTable
pedalsCompetency= {Math.random() * 100}
rightHandCompetency={Math.random() * 100}
leftHandCompetency= {Math.random() * 100}
accuracyCompetency= {Math.random() * 100}
arpegeCompetency= {Math.random() * 100}
chordsCompetency= {Math.random() * 100}
/>
<CompetenciesTable {...skillsQuery.data}/>
</Box>
</Box>
@@ -78,12 +78,14 @@ const HomeView = () => {
<SongCardGrid
heading={translate('recentlyPlayed')}
maxItemPerRow={2}
songs={[ ...Array(4).keys() ].map(() => ({
albumCover: "",
songTitle: "Song",
artistName: "Artist",
songId: 1
}))}
songs={playHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
</Box>
</Flex>
@@ -99,12 +101,14 @@ const HomeView = () => {
<SongCardGrid
maxItemPerRow={2}
heading={translate('lastSearched')}
songs={[ ...Array(4).keys() ].map(() => ({
albumCover: "",
songTitle: "Song",
artistName: "Artist",
songId: 1
}))}
songs={searchHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
</Box>
</Box>
+24 -51
View File
@@ -1,63 +1,36 @@
import React from "react";
import { useDispatch } from "../state/Store";
import { translate } from "../i18n/i18n";
import { Box, Button } from "native-base";
import React, { useEffect, useState } from "react";
import { Box } from "native-base";
import { useNavigation } from "@react-navigation/native";
import SearchBarSuggestions from "../components/SearchBarSuggestions";
import {
SuggestionList,
SuggestionType,
IllustratedSuggestionProps,
} from "../components/SearchBar";
const onTextSubmit = (text: string) => {
console.log(text);
};
import { useQueries, useQuery } from "react-query";
import { SuggestionType } from "../components/SearchBar";
import API from "../API";
const SearchView = () => {
const [query, setQuery] = useState<string>();
const navigation = useNavigation();
const IllustratedSuggestion: IllustratedSuggestionProps = {
text: "Love Story",
subtext: "Taylor Swift",
imageSrc:
"https://i.discogs.com/yHqu3pnLgJq-cVpYNVYu6mE-fbzIrmIRxc6vES5Oi48/rs:fit/g:sm/q:90/h:556/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTE2NjQ2/ODUwLTE2MDkwNDU5/NzQtNTkxOS5qcGVn.jpeg",
onPress: () => navigation.navigate("Song", { songId: 1 }),
};
// fill the suggestions with the data from the backend
const suggestions: SuggestionList = [
{
type: SuggestionType.ILLUSTRATED,
data: IllustratedSuggestion,
},
{
type: SuggestionType.ILLUSTRATED,
data: IllustratedSuggestion,
},
{
type: SuggestionType.ILLUSTRATED,
data: {
text: "Shed a Light",
subtext: "Robin Schulz & David Guetta",
imageSrc:
"https://imgs.search.brave.com/O9j2Z-oWiniq3lj7d-dAOgXLWCIqnHaFegmaSeIkWOY/rs:fit:560:320:1/g:ce/aHR0cHM6Ly91cGxv/YWQud2lraW1lZGlh/Lm9yZy93aWtpcGVk/aWEvZW4vdGh1bWIv/OC84ZS9TaGVkX2Ff/TGlnaHRfUm9iaW5f/U2NodWx6LmpwZy81/MTJweC1TaGVkX2Ff/TGlnaHRfUm9iaW5f/U2NodWx6LmpwZw",
onPress: () => navigation.navigate("Song", { songId: 1 }),
},
},
{
type: SuggestionType.TEXT,
data: {
text: "Lady Gaga",
onPress: () => navigation.navigate("Song", { songId: 1 }),
},
},
];
const searchQuery = useQuery(
['search', query],
() => API.searchSongs(query!),
{ enabled: query != undefined }
);
const artistsQueries = useQueries(searchQuery.data?.map((song) => (
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) }
)) ??[]);
return (
<Box style={{ padding: 10 }}>
<SearchBarSuggestions
onTextSubmit={onTextSubmit}
suggestions={suggestions}
onTextSubmit={setQuery}
suggestions={searchQuery.data?.map((searchResult) => ({
type: SuggestionType.ILLUSTRATED,
data: {
text: searchResult.name,
subtext: artistsQueries.find((artistQuery) => artistQuery.data?.id == searchResult.artistId)?.data?.name ?? "",
imageSrc: searchResult.cover,
onPress: () => navigation.navigate("Song", { songId: searchResult.id })
}
})) ?? []}
/>
</Box>
);
+4 -3
View File
@@ -2,8 +2,9 @@ import React from 'react';
import { View } from 'react-native';
import { Center, Button, Text, Switch, Slider, Select, Heading } from "native-base";
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { unsetUserToken } from '../state/UserSlice';
import { useDispatch, useSelector } from "react-redux";
import { unsetAPIAccess } from '../state/UserSlice';
import { useDispatch } from "react-redux";
import { useSelector } from '../state/Store';
import { useLanguage } from "../state/LanguageSlice";
import i18n, { AvailableLanguages, DefaultLanguage, translate } from "../i18n/i18n";
@@ -38,7 +39,7 @@ const MainView = ({navigation}) => {
{ translate('googleacctBtn')}
</Button>
<Button variant='ghost' onPress={() => dispatch(unsetUserToken())} >
<Button variant='ghost' onPress={() => dispatch(unsetAPIAccess())} >
{ translate('signoutBtn')}
</Button>
</Center>
+5 -5
View File
@@ -1,13 +1,13 @@
import { useRoute } from "@react-navigation/native";
import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon } from "native-base";
import API from "../API";
import { useQuery } from 'react-query';
import LoadingComponent from "../components/Loading";
import React, { useEffect, useState } from "react";
import logo from '../assets/cover.png';
import { translate } from "../i18n/i18n";
import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
import { useSelector } from "../state/Store";
import API from "../API";
interface SongLobbyProps {
// The unique identifier to find a song
@@ -34,12 +34,12 @@ const SongLobbyView = () => {
<Box style={{ padding: 30, flexDirection: 'column' }}>
<Box style={{ flexDirection: 'row', height: '30%'}}>
<Box style={{ flex: 3 }}>
<Image source={logo} style={{ height: '100%', width: undefined, resizeMode: 'contain' }}/>
<Image source={{ uri: songQuery.data!.cover }} style={{ height: '100%', width: undefined, resizeMode: 'contain' }}/>
</Box>
<Box style={{ flex: 0.5 }}/>
<Box style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<Box flex={1}>
<Text bold fontSize='lg'>{songQuery.data!.title}</Text>
<Text bold fontSize='lg'>{songQuery.data!.name}</Text>
<Text>{'3:20'} - {translate('level')} { chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length }</Text>
<Button width='auto' rightIcon={<Icon as={Ionicons} name="play-outline"/>}>{ translate('playBtn') }</Button>
</Box>
@@ -55,7 +55,7 @@ const SongLobbyView = () => {
<Text>{scoresQuery.data!.slice(-1)[0]!.score}</Text>
</Box>
</Box>
<Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text>
{/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */}
<Box flexDirection='row'>
<Button
variant='ghost'
+77
View File
@@ -2405,6 +2405,16 @@
resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa"
integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ==
"@react-navigation/core@^3.7.9":
version "3.7.9"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-3.7.9.tgz#3f7ba0fcb6c8d74a77a057382af198d84c7c4e3b"
integrity sha512-EknbzM8OI9A5alRxXtQRV5Awle68B+z1QAxNty5DxmlS3BNfmduWNGnim159ROyqxkuDffK9L/U/Tbd45mx+Jg==
dependencies:
hoist-non-react-statics "^3.3.2"
path-to-regexp "^1.8.0"
query-string "^6.13.6"
react-is "^16.13.0"
"@react-navigation/core@^6.4.0":
version "6.4.0"
resolved "https://registry.yarnpkg.com/@react-navigation/core/-/core-6.4.0.tgz#c44d33a8d8ef010a102c7f831fc8add772678509"
@@ -2430,6 +2440,14 @@
"@react-navigation/elements" "^1.3.6"
warn-once "^0.1.0"
"@react-navigation/native@^3.8.4":
version "3.8.4"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-3.8.4.tgz#4d77f86506364ecf18b33c7f8740afb6763d0b37"
integrity sha512-gXSVcL7bfFDyVkvyg1FiAqTCIgZub5K1X/TZqURBs2CPqDpfX1OsCtB9D33eTF14SpbfgHW866btqrrxoCACfg==
dependencies:
hoist-non-react-statics "^3.3.2"
react-native-safe-area-view "^0.14.9"
"@react-navigation/native@^6.0.11":
version "6.0.13"
resolved "https://registry.yarnpkg.com/@react-navigation/native/-/native-6.0.13.tgz#ec504120e193ea6a7f24ffa765a1338be5a3160a"
@@ -2976,6 +2994,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.2.tgz#17d42c6322d917764dd3d2d3a10d7884925de067"
integrity sha512-cRMwIgdDN43GO4xMWAfJAecYn8wV4JbsOGHNfNUIDiuYkUYAR5ec4Rj7IO2SAhFPEfpPtLtUTbbny/TCT7aDwA==
"@types/node@^18.11.8":
version "18.11.8"
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.8.tgz#16d222a58d4363a2a359656dd20b28414de5d265"
integrity sha512-uGwPWlE0Hj972KkHtCDVwZ8O39GmyjfMane1Z3GUBGGnkZ2USDq7SxLpVIiIHpweY9DS0QTDH0Nw7RNBsAAZ5A==
"@types/normalize-package-data@^2.4.0":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301"
@@ -3005,6 +3028,13 @@
dependencies:
"@types/react" "*"
"@types/react-navigation@^3.4.0":
version "3.4.0"
resolved "https://registry.yarnpkg.com/@types/react-navigation/-/react-navigation-3.4.0.tgz#d610d13c9162312079a8ca102660143f07432cbf"
integrity sha512-Y7F5zU8BTBK8tEOvUqgvwvPZ7+9vnc2UI1vHwJ/9ZJG98TntNv04GWa6lrn4MA4149pqw6cyNw/V49Yd2osAFQ==
dependencies:
react-navigation "*"
"@types/react-query@^1.2.9":
version "1.2.9"
resolved "https://registry.yarnpkg.com/@types/react-query/-/react-query-1.2.9.tgz#61df5a0594ea4b90234f9c0fdd8f12a06e331fed"
@@ -3440,6 +3470,11 @@ babel-plugin-syntax-trailing-function-commas@^7.0.0-beta.0:
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-7.0.0-beta.0.tgz#aa213c1435e2bffeb6fca842287ef534ad05d5cf"
integrity sha512-Xj9XuRuz3nTSbaTXWv3itLOcxyF4oPD8douBBmj7U9BBC6nEBYfyOJYQMf/8PJAFotC62UY5dFfIGEPr7WswzQ==
babel-plugin-transform-inline-environment-variables@^0.4.4:
version "0.4.4"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-environment-variables/-/babel-plugin-transform-inline-environment-variables-0.4.4.tgz#974245008b3cbbd646bd81707af147aea3acca43"
integrity sha512-bJILBtn5a11SmtR2j/3mBOjX4K3weC6cq+NNZ7hG22wCAqpc3qtj/iN7dSe9HDiS46lgp1nHsQgeYrea/RUe+g==
babel-preset-current-node-syntax@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b"
@@ -5358,6 +5393,11 @@ hermes-profile-transformer@^0.0.6:
dependencies:
source-map "^0.7.3"
hoist-non-react-statics@^2.3.1:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
@@ -5811,6 +5851,11 @@ is-wsl@^2.2.0:
dependencies:
is-docker "^2.0.0"
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isarray@1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@@ -7859,6 +7904,13 @@ path-parse@^1.0.5, path-parse@^1.0.7:
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
@@ -8050,6 +8102,16 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
query-string@^6.13.6:
version "6.14.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.14.1.tgz#7ac2dca46da7f309449ba0f86b1fd28255b0c86a"
integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==
dependencies:
decode-uri-component "^0.2.0"
filter-obj "^1.1.0"
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"
query-string@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.1.tgz#754620669db978625a90f635f12617c271a088e1"
@@ -8182,6 +8244,13 @@ react-native-safe-area-context@4.2.4:
resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.2.4.tgz#4df42819759c4d3c74252c8678c2772cfa2271a6"
integrity sha512-OOX+W2G4YYufvryonn6Kw6YnyT8ZThkxPHZBD04NLHaZmicUaaDVII/PZ3M5fD1o5N62+T+8K4bCS5Un2ggvkA==
react-native-safe-area-view@^0.14.9:
version "0.14.9"
resolved "https://registry.yarnpkg.com/react-native-safe-area-view/-/react-native-safe-area-view-0.14.9.tgz#90ee8383037010d9a5055a97cf97e4c1da1f0c3d"
integrity sha512-WII/ulhpVyL/qbYb7vydq7dJAfZRBcEhg4/UWt6F6nAKpLa3gAceMOxBxI914ppwSP/TdUsandFy6lkJQE0z4A==
dependencies:
hoist-non-react-statics "^2.3.1"
react-native-screens@~3.11.1:
version "3.11.1"
resolved "https://registry.yarnpkg.com/react-native-screens/-/react-native-screens-3.11.1.tgz#9bca9968986ca9195cb1e7e6fca37543bde64ecb"
@@ -8263,6 +8332,14 @@ react-native@0.68.2:
whatwg-fetch "^3.0.0"
ws "^6.1.4"
react-navigation@*:
version "4.4.4"
resolved "https://registry.yarnpkg.com/react-navigation/-/react-navigation-4.4.4.tgz#8cda2219196311db440e54998bc724523359949f"
integrity sha512-08Nzy1aKEd73496CsuzN49vLFmxPKYF5WpKGgGvkQ10clB79IRM2BtAfVl6NgPKuUM8FXq1wCsrjo/c5ftl5og==
dependencies:
"@react-navigation/core" "^3.7.9"
"@react-navigation/native" "^3.8.4"
react-query@*:
version "3.39.2"
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.39.2.tgz#9224140f0296f01e9664b78ed6e4f69a0cc9216f"