Merge branch 'main' into feature/adc/artist-view
This commit is contained in:
@@ -19,6 +19,14 @@
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-empty-function": "off"
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"name": "react-query",
|
||||
"importNames": ["useQuery", "useInfiniteQuery", "useQueries"],
|
||||
"message": "Use wrapper functions provided by Queries.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
492
front/API.ts
492
front/API.ts
@@ -1,6 +1,5 @@
|
||||
import Artist from './models/Artist';
|
||||
import Album from './models/Album';
|
||||
import AuthToken from './models/AuthToken';
|
||||
import Chapter from './models/Chapter';
|
||||
import Lesson from './models/Lesson';
|
||||
import Genre from './models/Genre';
|
||||
@@ -12,10 +11,11 @@ import Constants from 'expo-constants';
|
||||
import store from './state/Store';
|
||||
import { Platform } from 'react-native';
|
||||
import { en } from './i18n/Translations';
|
||||
import { QueryClient } from 'react-query';
|
||||
import UserSettings from './models/UserSettings';
|
||||
import { PartialDeep } from 'type-fest';
|
||||
import SearchHistory from './models/SearchHistory';
|
||||
import { Query } from './Queries';
|
||||
import CompetenciesTable from './components/CompetenciesTable';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
type RegistrationInput = AuthenticationInput & { email: string };
|
||||
@@ -72,7 +72,7 @@ export default class API {
|
||||
try {
|
||||
const jsonResponse = body.length != 0 ? JSON.parse(body) : {};
|
||||
if (!response.ok) {
|
||||
throw new APIError(jsonResponse ?? response.statusText, response.status);
|
||||
throw new APIError(response.statusText ?? body, response.status);
|
||||
}
|
||||
return jsonResponse;
|
||||
} catch (e) {
|
||||
@@ -137,43 +137,53 @@ export default class API {
|
||||
/***
|
||||
* Retrieve information of the currently authentified user
|
||||
*/
|
||||
public static async getUserInfo(): Promise<User> {
|
||||
const user = await API.fetch({
|
||||
route: '/auth/me',
|
||||
});
|
||||
|
||||
// this a dummy settings object, we will need to fetch the real one from the API
|
||||
public static getUserInfo(): Query<User> {
|
||||
return {
|
||||
id: user.id as number,
|
||||
name: (user.username ?? user.name) as string,
|
||||
email: user.email as string,
|
||||
premium: false,
|
||||
isGuest: user.isGuest as boolean,
|
||||
data: {
|
||||
gamesPlayed: user.partyPlayed as number,
|
||||
xp: 0,
|
||||
createdAt: new Date('2023-04-09T00:00:00.000Z'),
|
||||
avatar: 'https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn',
|
||||
key: 'user',
|
||||
exec: async () => {
|
||||
const user = await API.fetch({
|
||||
route: '/auth/me',
|
||||
});
|
||||
|
||||
// this a dummy settings object, we will need to fetch the real one from the API
|
||||
return {
|
||||
id: user.id as number,
|
||||
name: (user.username ?? user.name) as string,
|
||||
email: user.email as string,
|
||||
premium: false,
|
||||
isGuest: user.isGuest as boolean,
|
||||
data: {
|
||||
gamesPlayed: user.partyPlayed as number,
|
||||
xp: 0,
|
||||
createdAt: new Date('2023-04-09T00:00:00.000Z'),
|
||||
avatar: 'https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn',
|
||||
},
|
||||
} as User;
|
||||
},
|
||||
} as User;
|
||||
};
|
||||
}
|
||||
|
||||
public static async getUserSettings(): Promise<UserSettings> {
|
||||
const settings = await API.fetch({
|
||||
route: '/auth/me/settings',
|
||||
});
|
||||
|
||||
public static getUserSettings(): Query<UserSettings> {
|
||||
return {
|
||||
notifications: {
|
||||
pushNotif: settings.pushNotification,
|
||||
emailNotif: settings.emailNotification,
|
||||
trainNotif: settings.trainingNotification,
|
||||
newSongNotif: settings.newSongNotification,
|
||||
key: 'settings',
|
||||
exec: async () => {
|
||||
const settings = await API.fetch({
|
||||
route: '/auth/me/settings',
|
||||
});
|
||||
|
||||
return {
|
||||
notifications: {
|
||||
pushNotif: settings.pushNotification,
|
||||
emailNotif: settings.emailNotification,
|
||||
trainNotif: settings.trainingNotification,
|
||||
newSongNotif: settings.newSongNotification,
|
||||
},
|
||||
recommendations: settings.recommendations,
|
||||
weeklyReport: settings.weeklyReport,
|
||||
leaderBoard: settings.leaderBoard,
|
||||
showActivity: settings.showActivity,
|
||||
};
|
||||
},
|
||||
recommendations: settings.recommendations,
|
||||
weeklyReport: settings.weeklyReport,
|
||||
leaderBoard: settings.leaderBoard,
|
||||
showActivity: settings.showActivity,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -195,36 +205,62 @@ export default class API {
|
||||
});
|
||||
}
|
||||
|
||||
public static async getUserSkills() {
|
||||
public static getUserSkills(): Query<Parameters<typeof CompetenciesTable>[0]> {
|
||||
return {
|
||||
pedalsCompetency: Math.random() * 100,
|
||||
rightHandCompetency: Math.random() * 100,
|
||||
leftHandCompetency: Math.random() * 100,
|
||||
accuracyCompetency: Math.random() * 100,
|
||||
arpegeCompetency: Math.random() * 100,
|
||||
chordsCompetency: Math.random() * 100,
|
||||
key: 'skills',
|
||||
exec: async () => ({
|
||||
pedalsCompetency: Math.random() * 100,
|
||||
rightHandCompetency: Math.random() * 100,
|
||||
leftHandCompetency: Math.random() * 100,
|
||||
accuracyCompetency: Math.random() * 100,
|
||||
arpegeCompetency: Math.random() * 100,
|
||||
chordsCompetency: Math.random() * 100,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
public static getAllSongs(): Query<Song[]> {
|
||||
return {
|
||||
key: 'songs',
|
||||
exec: async () => {
|
||||
const songs = await API.fetch({
|
||||
route: '/song',
|
||||
});
|
||||
|
||||
// this is a dummy illustration, we will need to fetch the real one from the API
|
||||
return songs.data.map(
|
||||
// To be fixed with #168
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(song: any) =>
|
||||
({
|
||||
id: song.id as number,
|
||||
name: song.name as string,
|
||||
artistId: song.artistId as number,
|
||||
albumId: song.albumId as number,
|
||||
genreId: song.genreId as number,
|
||||
details: song.difficulties,
|
||||
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
|
||||
metrics: {},
|
||||
} as Song)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentify a new user through Google
|
||||
* Retrieve a song
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static async authWithGoogle(): Promise<AuthToken> {
|
||||
//TODO
|
||||
return '11111';
|
||||
}
|
||||
public static getSong(songId: number): Query<Song> {
|
||||
return {
|
||||
key: ['song', songId],
|
||||
exec: async () => {
|
||||
const song = await API.fetch({
|
||||
route: `/song/${songId}`,
|
||||
});
|
||||
|
||||
public static async getAllSongs(): Promise<Song[]> {
|
||||
const songs = await API.fetch({
|
||||
route: '/song',
|
||||
});
|
||||
|
||||
// this is a dummy illustration, we will need to fetch the real one from the API
|
||||
return songs.data.map(
|
||||
// To be fixed with #168
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(song: any) =>
|
||||
({
|
||||
// this is a dummy illustration, we will need to fetch the real one from the API
|
||||
return {
|
||||
id: song.id as number,
|
||||
name: song.name as string,
|
||||
artistId: song.artistId as number,
|
||||
@@ -232,65 +268,35 @@ export default class API {
|
||||
genreId: song.genreId as number,
|
||||
details: song.difficulties,
|
||||
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
|
||||
metrics: {},
|
||||
} as Song)
|
||||
);
|
||||
} as Song;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @description retrieves songs from a specific artist
|
||||
* @param artistId is the id of the artist that composed the songs aimed
|
||||
* @param skip is how much songs do we skip before returning the list
|
||||
* @param take is how much songs should be returned
|
||||
* @returns a Promise of Songs type array
|
||||
*/
|
||||
public static async getSongsByArtist(artistId: number): Promise<Song[]> {
|
||||
// let queryString = `/song?artisId=${artistId}`;
|
||||
|
||||
// if (skip) {
|
||||
// queryString = `${queryString}&skip=${skip}`;
|
||||
// }
|
||||
// if (take) {
|
||||
// queryString = `${queryString}&take=${take}`;
|
||||
// }
|
||||
// return await API.fetch({
|
||||
// route: queryString,
|
||||
// });
|
||||
|
||||
return API.fetch({
|
||||
route: `/song?artistId=${artistId}`,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a song
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static async getSong(songId: number): Promise<Song> {
|
||||
const song = await API.fetch({
|
||||
route: `/song/${songId}`,
|
||||
});
|
||||
|
||||
// this is a dummy illustration, we will need to fetch the real one from the API
|
||||
return {
|
||||
id: song.id as number,
|
||||
name: song.name as string,
|
||||
artistId: song.artistId as number,
|
||||
albumId: song.albumId as number,
|
||||
genreId: song.genreId as number,
|
||||
details: song.difficulties,
|
||||
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
|
||||
} as Song;
|
||||
}
|
||||
/**
|
||||
* Retrive a song's midi partition
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static async getSongMidi(songId: number): Promise<ArrayBuffer> {
|
||||
return API.fetch({
|
||||
route: `/song/${songId}/midi`,
|
||||
raw: true,
|
||||
});
|
||||
public static getSongMidi(songId: number): Query<ArrayBuffer> {
|
||||
return {
|
||||
key: ['midi', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/midi`,
|
||||
raw: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,119 +319,152 @@ export default class API {
|
||||
* Retrive a song's musicXML partition
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static async getSongMusicXML(songId: number): Promise<ArrayBuffer> {
|
||||
return API.fetch({
|
||||
route: `/song/${songId}/musicXml`,
|
||||
raw: true,
|
||||
});
|
||||
public static getSongMusicXML(songId: number): Query<ArrayBuffer> {
|
||||
return {
|
||||
key: ['musixml', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/musicXml`,
|
||||
raw: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive an artist
|
||||
*/
|
||||
public static async getArtist(artistId: number): Promise<Artist> {
|
||||
return API.fetch({
|
||||
route: `/artist/${artistId}`,
|
||||
});
|
||||
public static getArtist(artistId: number): Query<Artist> {
|
||||
return {
|
||||
key: ['artist', artistId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/artist/${artistId}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive a song's chapters
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static async getSongChapters(songId: number): Promise<Chapter[]> {
|
||||
return [1, 2, 3, 4, 5].map((value) => ({
|
||||
start: 100 * (value - 1),
|
||||
end: 100 * value,
|
||||
songId: songId,
|
||||
name: `Chapter ${value}`,
|
||||
type: 'chorus',
|
||||
key_aspect: 'rhythm',
|
||||
difficulty: value,
|
||||
id: value * 10,
|
||||
}));
|
||||
public static getSongChapters(songId: number): Query<Chapter[]> {
|
||||
return {
|
||||
key: ['chapters', songId],
|
||||
exec: async () =>
|
||||
[1, 2, 3, 4, 5].map((value) => ({
|
||||
start: 100 * (value - 1),
|
||||
end: 100 * value,
|
||||
songId: songId,
|
||||
name: `Chapter ${value}`,
|
||||
type: 'chorus',
|
||||
key_aspect: 'rhythm',
|
||||
difficulty: value,
|
||||
id: value * 10,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a song's play history
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static async getSongHistory(
|
||||
songId: number
|
||||
): Promise<{ best: number; history: SongHistory[] }> {
|
||||
return API.fetch({
|
||||
route: `/song/${songId}/history`,
|
||||
});
|
||||
public static getSongHistory(songId: number): Query<{ best: number; history: SongHistory[] }> {
|
||||
return {
|
||||
key: ['song', 'history', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/history`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a song by its name
|
||||
* @param query the string used to find the songs
|
||||
*/
|
||||
public static async searchSongs(query: string): Promise<Song[]> {
|
||||
return API.fetch({
|
||||
route: `/search/songs/${query}`,
|
||||
});
|
||||
public static searchSongs(query: string): Query<Song[]> {
|
||||
return {
|
||||
key: ['search', 'song', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/songs/${query}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search artists by name
|
||||
* @param query the string used to find the artists
|
||||
*/
|
||||
public static async searchArtists(query?: string): Promise<Artist[]> {
|
||||
return API.fetch({
|
||||
route: `/search/artists/${query}`,
|
||||
});
|
||||
public static searchArtists(query: string): Query<Artist[]> {
|
||||
return {
|
||||
key: ['search', 'artist', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/artists/${query}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Album by name
|
||||
* @param query the string used to find the album
|
||||
*/
|
||||
public static async searchAlbum(
|
||||
public static searchAlbum(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
query?: string
|
||||
): Promise<Album[]> {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Trooper',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kingdom Heart 365/2 OST',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Random Access Memories',
|
||||
},
|
||||
] as Album[];
|
||||
query: string
|
||||
): Query<Album[]> {
|
||||
return {
|
||||
key: ['search', 'album', query],
|
||||
exec: async () =>
|
||||
[
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Trooper',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kingdom Heart 365/2 OST',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Random Access Memories',
|
||||
},
|
||||
] as Album[],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve music genres
|
||||
*/
|
||||
public static async searchGenres(query?: string): Promise<Genre[]> {
|
||||
return API.fetch({
|
||||
route: `/search/genres/${query}`,
|
||||
});
|
||||
public static searchGenres(query: string): Query<Genre[]> {
|
||||
return {
|
||||
key: ['search', 'genre', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/genres/${query}`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a lesson
|
||||
* @param lessonId the id to find the lesson
|
||||
*/
|
||||
public static async getLesson(lessonId: number): Promise<Lesson> {
|
||||
public static getLesson(lessonId: number): Query<Lesson> {
|
||||
return {
|
||||
title: 'Song',
|
||||
description: 'A song',
|
||||
requiredLevel: 1,
|
||||
mainSkill: 'lead-head-change',
|
||||
id: lessonId,
|
||||
key: ['lesson', lessonId],
|
||||
exec: async () => ({
|
||||
title: 'Song',
|
||||
description: 'A song',
|
||||
requiredLevel: 1,
|
||||
mainSkill: 'lead-head-change',
|
||||
id: lessonId,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -435,26 +474,28 @@ export default class API {
|
||||
* @param take how much do we take to return
|
||||
* @returns Returns an array of history entries (temporary type any)
|
||||
*/
|
||||
public static async getSearchHistory(skip?: number, take?: number): Promise<SearchHistory[]> {
|
||||
return (
|
||||
(
|
||||
await API.fetch({
|
||||
public static getSearchHistory(skip?: number, take?: number): Query<SearchHistory[]> {
|
||||
return {
|
||||
key: ['search', 'history', 'skip', skip, 'take', take],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`,
|
||||
method: 'GET',
|
||||
})
|
||||
)
|
||||
// To be fixed with #168
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
.map((e: any) => {
|
||||
return {
|
||||
id: e.id,
|
||||
query: e.query,
|
||||
type: e.type,
|
||||
userId: e.userId,
|
||||
timestamp: new Date(e.searchDate),
|
||||
} as SearchHistory;
|
||||
})
|
||||
);
|
||||
}).then((value) =>
|
||||
value.map(
|
||||
// To be fixed with #168
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(e: any) =>
|
||||
({
|
||||
id: e.id,
|
||||
query: e.query,
|
||||
type: e.type,
|
||||
userId: e.userId,
|
||||
timestamp: new Date(e.searchDate),
|
||||
} as SearchHistory)
|
||||
)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -479,85 +520,38 @@ export default class API {
|
||||
* Retrieve the authenticated user's recommendations
|
||||
* @returns an array of songs
|
||||
*/
|
||||
public static async getSongSuggestions(): Promise<Song[]> {
|
||||
const queryClient = new QueryClient();
|
||||
return await queryClient.fetchQuery(['API', 'allsongs'], API.getAllSongs);
|
||||
public static getSongSuggestions(): Query<Song[]> {
|
||||
return API.getAllSongs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the authenticated user's play history
|
||||
* * @returns an array of songs
|
||||
*/
|
||||
public static async getUserPlayHistory(): Promise<SongHistory[]> {
|
||||
return this.fetch({
|
||||
route: '/history',
|
||||
});
|
||||
public static getUserPlayHistory(): Query<SongHistory[]> {
|
||||
return {
|
||||
key: ['history'],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: '/history',
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a lesson's history
|
||||
* @param lessonId the id to find the lesson
|
||||
*/
|
||||
public static async getLessonHistory(lessonId: number): Promise<LessonHistory[]> {
|
||||
return [
|
||||
{
|
||||
lessonId,
|
||||
userId: 1,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a partition images
|
||||
* @param _songId the id of the song
|
||||
* This API may be merged with the fetch song in the future
|
||||
*/
|
||||
public static async getPartitionRessources(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
songId: number
|
||||
): Promise<[string, number, number][]> {
|
||||
return [
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469560426545222/vivaldi_split_1.png',
|
||||
1868,
|
||||
400,
|
||||
public static getLessonHistory(lessonId: number): Query<LessonHistory[]> {
|
||||
return {
|
||||
key: ['lesson', 'history', lessonId],
|
||||
exec: async () => [
|
||||
{
|
||||
lessonId,
|
||||
userId: 1,
|
||||
},
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469560900505660/vivaldi_split_2.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469561261203506/vivaldi_split_3.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469561546424381/vivaldi_split_4.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469562058133564/vivaldi_split_5.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469562347528202/vivaldi_split_6.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469562792136815/vivaldi_split_7.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
[
|
||||
'https://media.discordapp.net/attachments/717080637038788731/1067469563073142804/vivaldi_split_8.png',
|
||||
1868,
|
||||
400,
|
||||
],
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
public static async updateUserEmail(newEmail: string): Promise<User> {
|
||||
|
||||
@@ -9,14 +9,9 @@ import { PersistGate } from 'redux-persist/integration/react';
|
||||
import LanguageGate from './i18n/LanguageGate';
|
||||
import ThemeProvider, { ColorSchemeProvider } from './Theme';
|
||||
import 'react-native-url-polyfill/auto';
|
||||
import { QueryRules } from './Queries';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
const queryClient = new QueryClient(QueryRules);
|
||||
|
||||
export default function App() {
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ParamListBase,
|
||||
useNavigation as navigationHook,
|
||||
} from '@react-navigation/native';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
|
||||
import { RootState, useSelector } from './state/Store';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -16,7 +16,7 @@ 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 { useQuery } from './Queries';
|
||||
import API, { APIError } from './API';
|
||||
import PlayView from './views/PlayView';
|
||||
import ScoreView from './views/ScoreView';
|
||||
@@ -27,24 +27,79 @@ import ArtistDetailsView from './views/ArtistDetailsView';
|
||||
import { Button, Center, VStack } from 'native-base';
|
||||
import { unsetAccessToken } from './state/UserSlice';
|
||||
import TextButton from './components/TextButton';
|
||||
import ErrorView from './views/ErrorView';
|
||||
|
||||
// Util function to hide route props in URL
|
||||
const removeMe = () => '';
|
||||
|
||||
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') } },
|
||||
Home: {
|
||||
component: HomeView,
|
||||
options: { title: translate('welcome'), headerLeft: null },
|
||||
link: '/',
|
||||
},
|
||||
Play: { component: PlayView, options: { title: translate('play') }, link: '/play/:songId' },
|
||||
Settings: {
|
||||
component: SetttingsNavigator,
|
||||
options: { title: 'Settings' },
|
||||
link: '/settings/:screen?',
|
||||
stringify: {
|
||||
screen: removeMe,
|
||||
},
|
||||
},
|
||||
Song: {
|
||||
component: SongLobbyView,
|
||||
options: { title: translate('play') },
|
||||
link: '/song/:songId',
|
||||
},
|
||||
Artist: {
|
||||
component: ArtistDetailsView,
|
||||
options: { title: translate('artistFilter') },
|
||||
link: '/artist/:artistId',
|
||||
},
|
||||
Score: {
|
||||
component: ScoreView,
|
||||
options: { title: translate('score'), headerLeft: null },
|
||||
link: undefined,
|
||||
},
|
||||
Search: {
|
||||
component: SearchView,
|
||||
options: { title: translate('search') },
|
||||
link: '/search/:query?',
|
||||
},
|
||||
Error: {
|
||||
component: ErrorView,
|
||||
options: { title: translate('error'), headerLeft: null },
|
||||
link: undefined,
|
||||
},
|
||||
User: { component: ProfileView, options: { title: translate('user') }, link: '/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 } },
|
||||
Start: {
|
||||
component: StartPageView,
|
||||
options: { title: 'Chromacase', headerShown: false },
|
||||
link: '/',
|
||||
},
|
||||
Login: {
|
||||
component: (params: RouteProps<{}>) =>
|
||||
AuthenticationView({ isSignup: false, ...params }),
|
||||
options: { title: translate('signInBtn') },
|
||||
link: '/login',
|
||||
},
|
||||
Signup: {
|
||||
component: (params: RouteProps<{}>) =>
|
||||
AuthenticationView({ isSignup: true, ...params }),
|
||||
options: { title: translate('signUpBtn') },
|
||||
link: '/signup',
|
||||
},
|
||||
Oops: {
|
||||
component: ProfileErrorView,
|
||||
options: { title: 'Oops', headerShown: false },
|
||||
link: undefined,
|
||||
},
|
||||
} as const);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -90,6 +145,29 @@ const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =
|
||||
/>
|
||||
));
|
||||
|
||||
const routesToLinkingConfig = (
|
||||
routes: Partial<
|
||||
Record<keyof AppRouteParams, { link?: string; stringify?: Record<string, () => string> }>
|
||||
>
|
||||
) => {
|
||||
// Too lazy to (find the) type
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pagesToRoute = {} as Record<keyof AppRouteParams, any>;
|
||||
Object.keys(routes).forEach((route) => {
|
||||
const index = route as keyof AppRouteParams;
|
||||
if (routes[index]?.link) {
|
||||
pagesToRoute[index] = {
|
||||
path: routes[index]!.link!,
|
||||
stringify: routes[index]!.stringify,
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
prefixes: [],
|
||||
config: { screens: pagesToRoute },
|
||||
};
|
||||
};
|
||||
|
||||
const ProfileErrorView = (props: { onTryAgain: () => void }) => {
|
||||
const dispatch = useDispatch();
|
||||
return (
|
||||
@@ -113,7 +191,7 @@ const ProfileErrorView = (props: { onTryAgain: () => void }) => {
|
||||
export const Router = () => {
|
||||
const dispatch = useDispatch();
|
||||
const accessToken = useSelector((state: RootState) => state.user.accessToken);
|
||||
const userProfile = useQuery(['user', 'me', accessToken], () => API.getUserInfo(), {
|
||||
const userProfile = useQuery(API.getUserInfo, {
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (err) => {
|
||||
@@ -123,6 +201,24 @@ export const Router = () => {
|
||||
},
|
||||
});
|
||||
const colorScheme = useColorScheme();
|
||||
const authStatus = useMemo(() => {
|
||||
if (userProfile.isError && accessToken && !userProfile.isLoading) {
|
||||
return 'error';
|
||||
}
|
||||
if (userProfile.isLoading && !userProfile.data) {
|
||||
return 'loading';
|
||||
}
|
||||
if (userProfile.isSuccess && accessToken) {
|
||||
return 'authed';
|
||||
}
|
||||
return 'noAuth';
|
||||
}, [userProfile, accessToken]);
|
||||
const routes = useMemo(() => {
|
||||
if (authStatus == 'authed') {
|
||||
return protectedRoutes();
|
||||
}
|
||||
return publicRoutes();
|
||||
}, [authStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
@@ -130,22 +226,27 @@ export const Router = () => {
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
if (authStatus == 'loading') {
|
||||
// We dont want this to be a screen, as this lead to a navigator without the requested route, and fallback.
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationContainer theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}>
|
||||
<NavigationContainer
|
||||
linking={routesToLinkingConfig(routes)}
|
||||
fallback={<LoadingView />}
|
||||
theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}
|
||||
>
|
||||
<Stack.Navigator>
|
||||
{userProfile.isError && accessToken && !userProfile.isLoading ? (
|
||||
{authStatus == 'error' ? (
|
||||
<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()
|
||||
)
|
||||
routesToScreens(routes)
|
||||
)}
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
|
||||
80
front/Queries.ts
Normal file
80
front/Queries.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
// Disabled for obvious reasons
|
||||
import * as RQ from 'react-query';
|
||||
|
||||
const QueryRules: RQ.QueryClientConfig = {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
// This is needed explicitly, otherwise will refetch **all** the time
|
||||
staleTime: Infinity,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// The options of the query.
|
||||
// E.g. enabled.
|
||||
type QueryOptions<T> = RQ.UseQueryOptions<T>;
|
||||
|
||||
// What a query *is*
|
||||
export type Query<ReturnType> = {
|
||||
key: RQ.QueryKey;
|
||||
exec: () => Promise<ReturnType>;
|
||||
};
|
||||
|
||||
// We want `useQuery`/`ies` to accept either a function or a `Query` directly
|
||||
type QueryOrQueryFn<T> = Query<T> | (() => Query<T>);
|
||||
// A simple util function to avoid conditions everywhere
|
||||
const queryToFn = <T>(q: QueryOrQueryFn<T>) => {
|
||||
if (typeof q === 'function') {
|
||||
return q;
|
||||
}
|
||||
return () => q;
|
||||
};
|
||||
|
||||
// This also allows lazy laoding of query function.
|
||||
// I.e. not call the function before it is enabled;
|
||||
const buildRQuery = <T, Opts extends QueryOptions<T>>(q: QueryOrQueryFn<T>, opts?: Opts) => {
|
||||
const laziedQuery = queryToFn(q);
|
||||
if (opts?.enabled === false) {
|
||||
return {
|
||||
queryKey: [],
|
||||
// This will not be called because the query is disabled.
|
||||
// However, this is done for type-safety
|
||||
queryFn: () => laziedQuery().exec(),
|
||||
...opts,
|
||||
};
|
||||
}
|
||||
const resolvedQuery = laziedQuery();
|
||||
return {
|
||||
queryKey: resolvedQuery.key,
|
||||
queryFn: resolvedQuery.exec,
|
||||
...opts,
|
||||
};
|
||||
};
|
||||
|
||||
const useQuery = <ReturnType, Opts extends QueryOptions<ReturnType>>(
|
||||
query: QueryOrQueryFn<ReturnType>,
|
||||
options?: Opts
|
||||
) => {
|
||||
return RQ.useQuery<ReturnType>(buildRQuery(query, options));
|
||||
};
|
||||
|
||||
const transformQuery = <OldReturnType, NewReturnType>(
|
||||
query: Query<OldReturnType>,
|
||||
fn: (res: OldReturnType) => NewReturnType
|
||||
) => {
|
||||
return {
|
||||
key: query.key,
|
||||
exec: () => query.exec().then(fn),
|
||||
};
|
||||
};
|
||||
|
||||
const useQueries = <ReturnTypes>(
|
||||
queries: readonly QueryOrQueryFn<ReturnTypes>[],
|
||||
options?: QueryOptions<ReturnTypes>
|
||||
) => {
|
||||
return RQ.useQueries(queries.map((q) => buildRQuery(q, options)));
|
||||
};
|
||||
|
||||
export { useQuery, useQueries, QueryRules, transformQuery };
|
||||
@@ -1,13 +1,26 @@
|
||||
import { useTheme } from 'native-base';
|
||||
import { Center, Spinner } from 'native-base';
|
||||
import useColorScheme from '../hooks/colorScheme';
|
||||
import { DefaultTheme, DarkTheme } from '@react-navigation/native';
|
||||
import { useMemo } from 'react';
|
||||
const LoadingComponent = () => {
|
||||
const theme = useTheme();
|
||||
return <Spinner color={theme.colors.primary[500]} />;
|
||||
};
|
||||
|
||||
const LoadingView = () => {
|
||||
const colorScheme = useColorScheme();
|
||||
const bgColor = useMemo(() => {
|
||||
switch (colorScheme) {
|
||||
case 'light':
|
||||
return DefaultTheme.colors.background;
|
||||
case 'dark':
|
||||
return DarkTheme.colors.background;
|
||||
}
|
||||
}, [colorScheme]);
|
||||
|
||||
return (
|
||||
<Center style={{ flexGrow: 1 }}>
|
||||
<Center style={{ flexGrow: 1, backgroundColor: bgColor }}>
|
||||
<LoadingComponent />
|
||||
</Center>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
HStack,
|
||||
VStack,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import { SafeAreaView, useColorScheme } from 'react-native';
|
||||
import { RootState, useSelector } from '../state/Store';
|
||||
import { SearchContext } from '../views/SearchView';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQueries, useQuery } from '../Queries';
|
||||
import { translate } from '../i18n/i18n';
|
||||
import API from '../API';
|
||||
import LoadingComponent from './Loading';
|
||||
@@ -27,8 +27,8 @@ import CardGridCustom from './CardGridCustom';
|
||||
import TextButton from './TextButton';
|
||||
import SearchHistoryCard from './HistoryCard';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import { getSongWArtistSuggestions } from './utils/api';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import Artist from '../models/Artist';
|
||||
|
||||
const swaToSongCardProps = (song: SongWithArtist) => ({
|
||||
songId: song.id,
|
||||
@@ -135,18 +135,38 @@ SongRow.defaultProps = {
|
||||
const HomeSearchComponent = () => {
|
||||
const { updateStringQuery } = React.useContext(SearchContext);
|
||||
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
|
||||
'history',
|
||||
() => API.getSearchHistory(0, 12),
|
||||
API.getSearchHistory(0, 12),
|
||||
{ enabled: true }
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingSuggestions, data: suggestionsData = [] } = useQuery(
|
||||
'suggestions',
|
||||
() => getSongWArtistSuggestions(),
|
||||
{
|
||||
enabled: true,
|
||||
}
|
||||
const songSuggestions = useQuery(API.getSongSuggestions);
|
||||
const songArtistSuggestions = useQueries(
|
||||
songSuggestions.data
|
||||
?.filter((song) => song.artistId !== null)
|
||||
.map(({ artistId }) => API.getArtist(artistId)) ?? []
|
||||
);
|
||||
const isLoadingSuggestions = useMemo(
|
||||
() => songSuggestions.isLoading || songArtistSuggestions.some((q) => q.isLoading),
|
||||
[songSuggestions, songArtistSuggestions]
|
||||
);
|
||||
const suggestionsData = useMemo(() => {
|
||||
if (isLoadingSuggestions) {
|
||||
return [];
|
||||
}
|
||||
return (
|
||||
songSuggestions.data
|
||||
?.map((song): [Song, Artist | undefined] => [
|
||||
song,
|
||||
songArtistSuggestions
|
||||
.map((q) => q.data)
|
||||
.filter((d) => d !== undefined)
|
||||
.find((data) => data?.id === song.artistId),
|
||||
])
|
||||
// We do not need the song
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
.filter(([song, artist]) => artist !== undefined)
|
||||
.map(([song, artist]) => ({ ...song, artist: artist! })) ?? []
|
||||
);
|
||||
}, [songSuggestions, songArtistSuggestions]);
|
||||
|
||||
return (
|
||||
<VStack mt="5" style={{ overflow: 'hidden' }}>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import API from '../../API';
|
||||
import { SongWithArtist } from '../../models/Song';
|
||||
|
||||
export const getSongWArtistSuggestions = async () => {
|
||||
const nextStepQuery = await API.getSongSuggestions();
|
||||
|
||||
const songWartist = await Promise.all(
|
||||
nextStepQuery.map(async (song) => {
|
||||
if (!song.artistId) throw new Error('Song has no artistId');
|
||||
const artist = await API.getArtist(song.artistId);
|
||||
return { ...song, artist } as SongWithArtist;
|
||||
})
|
||||
);
|
||||
return songWartist;
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
|
||||
const useUserSettings = () => {
|
||||
const queryKey = ['settings'];
|
||||
const settings = useQuery(queryKey, () => API.getUserSettings());
|
||||
const settings = useQuery(API.getUserSettings);
|
||||
const updateSettings = (...params: Parameters<typeof API.updateUserSettings>) =>
|
||||
API.updateUserSettings(...params).then(() => settings.refetch());
|
||||
return { settings, updateSettings };
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const en = {
|
||||
error: 'Error',
|
||||
goBackHome: 'Go Back Home',
|
||||
anErrorOccured: 'An Error Occured',
|
||||
welcome: 'Welcome',
|
||||
welcomeMessage: 'Welcome back ',
|
||||
signOutBtn: 'Sign out',
|
||||
@@ -179,6 +182,9 @@ export const en = {
|
||||
};
|
||||
|
||||
export const fr: typeof en = {
|
||||
error: 'Erreur',
|
||||
goBackHome: "Retourner à l'accueil",
|
||||
anErrorOccured: 'Une erreur est survenue',
|
||||
welcome: 'Bienvenue',
|
||||
welcomeMessage: 'Re-Bonjour ',
|
||||
signOutBtn: 'Se déconnecter',
|
||||
@@ -357,6 +363,9 @@ export const fr: typeof en = {
|
||||
};
|
||||
|
||||
export const sp: typeof en = {
|
||||
error: 'Error',
|
||||
anErrorOccured: 'ocurrió un error',
|
||||
goBackHome: 'regresar a casa',
|
||||
welcomeMessage: 'Benvenido',
|
||||
signOutBtn: 'Desconectarse',
|
||||
signInBtn: 'Connectarse',
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
type AuthToken = string;
|
||||
|
||||
export default AuthToken;
|
||||
@@ -1,64 +1,67 @@
|
||||
import { VStack, Text, Box, Image, Heading, IconButton, Icon, Container, Center, useBreakpointValue } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { useQuery } from 'react-query';
|
||||
import LoadingComponent from '../components/Loading';
|
||||
import { useQuery } from '../Queries';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import API from '../API';
|
||||
import Song from '../models/Song';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import SongRow from '../components/SongRow';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Key, useEffect, useState } from 'react';
|
||||
import { useNavigation } from '../Navigation';
|
||||
|
||||
const ArtistDetailsView = ({ artistId }: any) => {
|
||||
const { isLoading: isLoadingArtist, data: artistData, error: errorArtist } = useQuery(['artist', artistId], () => API.getArtist(artistId));
|
||||
// const { isLoading: isLoadingSongs, data: songData = [], error: errorSongs } = useQuery(['songs', artistId], () => API.getSongsByArtist(artistId))
|
||||
const screenSize = useBreakpointValue({ base: "small", md: "big" });
|
||||
const { isLoading, data: artistData, isError } = useQuery(API.getArtist(artistId));
|
||||
// const { isLoading: isLoadingSongs, data: songData = [], error: errorSongs } = useQuery(['songs', artistId], () => API.getSongsByArtist(artistId))
|
||||
const screenSize = useBreakpointValue({ base: "small", md: "big" });
|
||||
const isMobileView = screenSize == "small";
|
||||
const navigation = useNavigation();
|
||||
const [merde, setMerde] = useState<any>(null);
|
||||
const navigation = useNavigation();
|
||||
const [merde, setMerde] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Code to be executed when the component is focused
|
||||
console.warn('Component focused!');
|
||||
setMerde(API.getSongsByArtist(112));
|
||||
// Call your function or perform any other actions here
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
// Code to be executed when the component is focused
|
||||
console.warn('Component focused!');
|
||||
setMerde(API.getSongsByArtist(112));
|
||||
// Call your function or perform any other actions here
|
||||
}, []);
|
||||
|
||||
if (isLoadingArtist) {
|
||||
return <Center m={10} ><LoadingComponent /></Center>;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Box>
|
||||
<Image
|
||||
source={{ uri: 'https://picsum.photos/200' }}
|
||||
alt={artistData?.name}
|
||||
size={'100%'}
|
||||
height={isMobileView ? 200 : 300}
|
||||
width={'100%'}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
<Box>
|
||||
<Heading m={3} >Abba</Heading>
|
||||
<Box>
|
||||
{merde.map((comp, index) => (
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, "song", Date.now());
|
||||
navigation.navigate("Song", { songId: comp.id });
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Box>
|
||||
if (isError) {
|
||||
navigation.navigate('Error');
|
||||
}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
</SafeAreaView>
|
||||
);
|
||||
return (
|
||||
<SafeAreaView>
|
||||
<Box>
|
||||
<Image
|
||||
source={{ uri: 'https://picsum.photos/200' }}
|
||||
alt={artistData?.name}
|
||||
size={'100%'}
|
||||
height={isMobileView ? 200 : 300}
|
||||
width={'100%'}
|
||||
resizeMode='cover'
|
||||
/>
|
||||
<Box>
|
||||
<Heading m={3} >Abba</Heading>
|
||||
<Box>
|
||||
{merde.map((comp: Song | SongWithArtist, index: Key | null | undefined) => (
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, "song", Date.now());
|
||||
navigation.navigate("Song", { songId: comp.id });
|
||||
}}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</SafeAreaView>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArtistDetailsView;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Center, Button, Text } from 'native-base';
|
||||
import SigninForm from '../components/forms/signinform';
|
||||
import SignupForm from '../components/forms/signupform';
|
||||
import TextButton from '../components/TextButton';
|
||||
import { RouteProps } from '../Navigation';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
|
||||
const hanldeSignin = async (
|
||||
username: string,
|
||||
@@ -48,7 +48,8 @@ type AuthenticationViewProps = {
|
||||
|
||||
const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) => {
|
||||
const dispatch = useDispatch();
|
||||
const [mode, setMode] = React.useState<'signin' | 'signup'>(isSignup ? 'signup' : 'signin');
|
||||
const navigation = useNavigation();
|
||||
const mode = isSignup ? 'signup' : 'signin';
|
||||
|
||||
return (
|
||||
<Center style={{ flex: 1 }}>
|
||||
@@ -82,7 +83,7 @@ const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) =
|
||||
variant="outline"
|
||||
marginTop={5}
|
||||
colorScheme="primary"
|
||||
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
||||
onPress={() => navigation.navigate(mode === 'signin' ? 'Signup' : 'Login', {})}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
|
||||
19
front/views/ErrorView.tsx
Normal file
19
front/views/ErrorView.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Button, Center, VStack } from 'native-base';
|
||||
import Translate from '../components/Translate';
|
||||
import { useNavigation } from '../Navigation';
|
||||
|
||||
const ErrorView = () => {
|
||||
const navigation = useNavigation();
|
||||
return (
|
||||
<Center style={{ flexGrow: 1 }}>
|
||||
<VStack space={3} alignItems="center">
|
||||
<Translate translationKey="anErrorOccured" />
|
||||
<Button onPress={() => navigation.navigate('Home')}>
|
||||
<Translate translationKey="goBackHome" />
|
||||
</Button>
|
||||
</VStack>
|
||||
</Center>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorView;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useQueries, useQuery } from 'react-query';
|
||||
import { useQueries, useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import { Box, ScrollView, Flex, Stack, Heading, VStack, HStack } from 'native-base';
|
||||
@@ -14,26 +14,20 @@ import { FontAwesome5 } from '@expo/vector-icons';
|
||||
|
||||
const HomeView = () => {
|
||||
const navigation = useNavigation();
|
||||
const userQuery = useQuery(['user'], () => API.getUserInfo());
|
||||
const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory());
|
||||
const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory(0, 10));
|
||||
const skillsQuery = useQuery(['skills'], () => API.getUserSkills());
|
||||
const nextStepQuery = useQuery(['user', 'recommendations'], () => API.getSongSuggestions());
|
||||
const userQuery = useQuery(API.getUserInfo);
|
||||
const playHistoryQuery = useQuery(API.getUserPlayHistory);
|
||||
const searchHistoryQuery = useQuery(API.getSearchHistory(0, 10));
|
||||
const skillsQuery = useQuery(API.getUserSkills);
|
||||
const nextStepQuery = useQuery(API.getSongSuggestions);
|
||||
const songHistory = useQueries(
|
||||
playHistoryQuery.data?.map(({ songID }) => ({
|
||||
queryKey: ['song', songID],
|
||||
queryFn: () => API.getSong(songID),
|
||||
})) ?? []
|
||||
playHistoryQuery.data?.map(({ songID }) => API.getSong(songID)) ?? []
|
||||
);
|
||||
const artistsQueries = useQueries(
|
||||
songHistory
|
||||
.map((entry) => entry.data)
|
||||
.concat(nextStepQuery.data ?? [])
|
||||
.filter((s): s is Song => s !== undefined)
|
||||
.map((song) => ({
|
||||
queryKey: ['artist', song.id],
|
||||
queryFn: () => API.getArtist(song.artistId),
|
||||
}))
|
||||
.map((song) => API.getArtist(song.artistId))
|
||||
);
|
||||
|
||||
if (
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import IconButton from '../components/IconButton';
|
||||
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import { useQuery } from 'react-query';
|
||||
import { transformQuery, useQuery } from '../Queries';
|
||||
import API from '../API';
|
||||
import LoadingComponent, { LoadingView } from '../components/Loading';
|
||||
import Constants from 'expo-constants';
|
||||
@@ -72,7 +72,7 @@ function parseMidiMessage(message: MIDIMessageEvent) {
|
||||
const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
const accessToken = useSelector((state: RootState) => state.user.accessToken);
|
||||
const navigation = useNavigation();
|
||||
const song = useQuery(['song', songId], () => API.getSong(songId), { staleTime: Infinity });
|
||||
const song = useQuery(API.getSong(songId), { staleTime: Infinity });
|
||||
const toast = useToast();
|
||||
const [lastScoreMessage, setLastScoreMessage] = useState<ScoreMessage>();
|
||||
const webSocket = useRef<WebSocket>();
|
||||
@@ -84,8 +84,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
|
||||
const [score, setScore] = useState(0); // Between 0 and 100
|
||||
const fadeAnim = useRef(new Animated.Value(0)).current;
|
||||
const musixml = useQuery(
|
||||
['musixml', songId],
|
||||
() => API.getSongMusicXML(songId).then((data) => new TextDecoder().decode(data)),
|
||||
transformQuery(API.getSongMusicXML(songId), (data) => new TextDecoder().decode(data)),
|
||||
{ staleTime: Infinity }
|
||||
);
|
||||
const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000;
|
||||
|
||||
@@ -120,7 +120,7 @@ const ProfileView = () => {
|
||||
<PlayerStats />
|
||||
<Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50} zIndex={1}>
|
||||
<TextButton
|
||||
onPress={() => navigation.navigate('Settings', { screen: 'Profile' })}
|
||||
onPress={() => navigation.navigate('Settings', { screen: 'profile' })}
|
||||
translate={{ translationKey: 'settingsBtn' }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -6,7 +6,7 @@ import TextButton from '../components/TextButton';
|
||||
import API from '../API';
|
||||
import CardGridCustom from '../components/CardGridCustom';
|
||||
import SongCard from '../components/SongCard';
|
||||
import { useQueries, useQuery } from 'react-query';
|
||||
import { useQueries, useQuery } from '../Queries';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
|
||||
type ScoreViewProps = {
|
||||
@@ -25,25 +25,18 @@ type ScoreViewProps = {
|
||||
};
|
||||
};
|
||||
|
||||
const ScoreView = ({ songId, overallScore, precision, score }: RouteProps<ScoreViewProps>) => {
|
||||
const ScoreView = (props: RouteProps<ScoreViewProps>) => {
|
||||
const { songId, overallScore, precision, score } = props;
|
||||
const navigation = useNavigation();
|
||||
const songQuery = useQuery(['song', songId], () => API.getSong(songId));
|
||||
const artistQuery = useQuery(
|
||||
['song', songId, 'artist'],
|
||||
() => API.getArtist(songQuery.data!.artistId!),
|
||||
{
|
||||
enabled: songQuery.data != undefined,
|
||||
}
|
||||
);
|
||||
// const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId));
|
||||
const recommendations = useQuery(['song', 'recommendations'], () => API.getSongSuggestions());
|
||||
const songQuery = useQuery(API.getSong(songId));
|
||||
const artistQuery = useQuery(() => API.getArtist(songQuery.data!.artistId!), {
|
||||
enabled: songQuery.data !== undefined,
|
||||
});
|
||||
const recommendations = useQuery(API.getSongSuggestions);
|
||||
const artistRecommendations = useQueries(
|
||||
recommendations.data
|
||||
?.filter(({ artistId }) => artistId !== null)
|
||||
.map((song) => ({
|
||||
queryKey: ['artist', song.artistId],
|
||||
queryFn: () => API.getArtist(song.artistId!),
|
||||
})) ?? []
|
||||
.map((song) => API.getArtist(song.artistId)) ?? []
|
||||
);
|
||||
|
||||
if (
|
||||
@@ -54,6 +47,10 @@ const ScoreView = ({ songId, overallScore, precision, score }: RouteProps<ScoreV
|
||||
) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
if (songQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
|
||||
|
||||
@@ -4,7 +4,7 @@ import Artist from '../models/Artist';
|
||||
import Song from '../models/Song';
|
||||
import Genre from '../models/Genre';
|
||||
import API from '../API';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '../Queries';
|
||||
import { SearchResultComponent } from '../components/SearchResult';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { Filter } from '../components/SearchBar';
|
||||
@@ -46,20 +46,17 @@ const SearchView = (props: RouteProps<SearchViewProps>) => {
|
||||
const [stringQuery, setStringQuery] = useState<string>(props?.query ?? '');
|
||||
|
||||
const { isLoading: isLoadingSong, data: songData = [] } = useQuery(
|
||||
['song', stringQuery],
|
||||
() => API.getSongsByArtist(112),
|
||||
API.searchSongs(stringQuery),
|
||||
{ enabled: !!stringQuery }
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingArtist, data: artistData = [] } = useQuery(
|
||||
['artist', stringQuery],
|
||||
() => API.searchArtists(stringQuery),
|
||||
API.searchArtists(stringQuery),
|
||||
{ enabled: !!stringQuery }
|
||||
);
|
||||
|
||||
const { isLoading: isLoadingGenre, data: genreData = [] } = useQuery(
|
||||
['genre', stringQuery],
|
||||
() => API.searchGenres(stringQuery),
|
||||
API.searchGenres(stringQuery),
|
||||
{ enabled: !!stringQuery }
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Divider, Box, Image, Text, VStack, PresenceTransition, Icon, Stack } from 'native-base';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '../Queries';
|
||||
import LoadingComponent, { LoadingView } from '../components/Loading';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Translate, translate } from '../i18n/i18n';
|
||||
@@ -16,19 +16,22 @@ interface SongLobbyProps {
|
||||
|
||||
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
|
||||
const navigation = useNavigation();
|
||||
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)
|
||||
);
|
||||
// Refetch to update score when coming back from score view
|
||||
const songQuery = useQuery(API.getSong(props.songId), { refetchOnWindowFocus: true });
|
||||
const chaptersQuery = useQuery(API.getSongChapters(props.songId), {
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
const scoresQuery = useQuery(API.getSongHistory(props.songId), { refetchOnWindowFocus: true });
|
||||
const [chaptersOpen, setChaptersOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
if (chaptersOpen && !chaptersQuery.data) chaptersQuery.refetch();
|
||||
}, [chaptersOpen]);
|
||||
useEffect(() => {}, [songQuery.isLoading]);
|
||||
if (songQuery.isLoading || scoresQuery.isLoading) return <LoadingView />;
|
||||
if (songQuery.isError || scoresQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<Box style={{ padding: 30, flexDirection: 'column' }}>
|
||||
<Box style={{ flexDirection: 'row', height: '30%' }}>
|
||||
|
||||
@@ -92,7 +92,7 @@ const StartPageView = () => {
|
||||
image={imgLogin}
|
||||
iconName="user"
|
||||
iconProvider={FontAwesome5}
|
||||
onPress={() => navigation.navigate('Login', { isSignup: false })}
|
||||
onPress={() => navigation.navigate('Login', {})}
|
||||
style={{
|
||||
width: isSmallScreen ? '90%' : 'clamp(100px, 33.3%, 600px)',
|
||||
height: '300px',
|
||||
@@ -132,7 +132,7 @@ const StartPageView = () => {
|
||||
subtitle="Create an account to save your progress"
|
||||
iconProvider={FontAwesome5}
|
||||
iconName="user-plus"
|
||||
onPress={() => navigation.navigate('Login', { isSignup: true })}
|
||||
onPress={() => navigation.navigate('Signup', {})}
|
||||
style={{
|
||||
height: '150px',
|
||||
width: isSmallScreen ? '90%' : 'clamp(150px, 50%, 600px)',
|
||||
|
||||
@@ -7,7 +7,7 @@ import TextButton from '../../components/TextButton';
|
||||
import { LoadingView } from '../../components/Loading';
|
||||
import ElementList from '../../components/GtkUI/ElementList';
|
||||
import { translate } from '../../i18n/i18n';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '../../Queries';
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name
|
||||
@@ -19,7 +19,7 @@ const getInitials = (name: string) => {
|
||||
// Too painful to infer the settings-only, typed navigator. Gave up
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ProfileSettings = ({ navigation }: { navigation: any }) => {
|
||||
const userQuery = useQuery(['user'], () => API.getUserInfo());
|
||||
const userQuery = useQuery(API.getUserInfo);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!userQuery.data || userQuery.isLoading) {
|
||||
|
||||
@@ -10,8 +10,9 @@ import NotificationsView from './NotificationView';
|
||||
import PrivacyView from './PrivacyView';
|
||||
import PreferencesView from './PreferencesView';
|
||||
import GuestToUserView from './GuestToUserView';
|
||||
import { useQuery } from 'react-query';
|
||||
import { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import { RouteProps } from '../../Navigation';
|
||||
|
||||
const handleChangeEmail = async (newEmail: string): Promise<string> => {
|
||||
await API.updateUserEmail(newEmail);
|
||||
@@ -65,18 +66,18 @@ const TabRow = createTabRowNavigator();
|
||||
|
||||
type SetttingsNavigatorProps = {
|
||||
screen?:
|
||||
| 'Profile'
|
||||
| 'Preferences'
|
||||
| 'Notifications'
|
||||
| 'Privacy'
|
||||
| 'ChangePassword'
|
||||
| 'ChangeEmail'
|
||||
| 'GoogleAccount'
|
||||
| 'PianoSettings';
|
||||
| 'profile'
|
||||
| 'preferences'
|
||||
| 'notifications'
|
||||
| 'privacy'
|
||||
| 'changePassword'
|
||||
| 'changeEmail'
|
||||
| 'googleAccount'
|
||||
| 'pianoSettings';
|
||||
};
|
||||
|
||||
const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
const userQuery = useQuery(['user'], () => API.getUserInfo());
|
||||
const SetttingsNavigator = (props?: RouteProps<SetttingsNavigatorProps>) => {
|
||||
const userQuery = useQuery(API.getUserInfo);
|
||||
const user = useMemo(() => userQuery.data, [userQuery]);
|
||||
|
||||
if (userQuery.isLoading) {
|
||||
@@ -98,7 +99,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
<TabRow.Screen name="InternalDefault" component={Box} />
|
||||
{user && user.isGuest && (
|
||||
<TabRow.Screen
|
||||
name="GuestToUser"
|
||||
name="guestToUser"
|
||||
component={GuestToUserView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryGuest'),
|
||||
@@ -108,7 +109,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
/>
|
||||
)}
|
||||
<TabRow.Screen
|
||||
name="Profile"
|
||||
name="profile"
|
||||
component={ProfileSettings}
|
||||
options={{
|
||||
title: translate('SettingsCategoryProfile'),
|
||||
@@ -117,7 +118,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="Preferences"
|
||||
name="preferences"
|
||||
component={PreferencesView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryPreferences'),
|
||||
@@ -126,7 +127,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="Notifications"
|
||||
name="notifications"
|
||||
component={NotificationsView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryNotifications'),
|
||||
@@ -135,7 +136,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="Privacy"
|
||||
name="privacy"
|
||||
component={PrivacyView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryPrivacy'),
|
||||
@@ -144,7 +145,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="ChangePassword"
|
||||
name="changePassword"
|
||||
component={ChangePasswordView}
|
||||
options={{
|
||||
title: translate('SettingsCategorySecurity'),
|
||||
@@ -153,7 +154,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="ChangeEmail"
|
||||
name="changeEmail"
|
||||
component={ChangeEmailView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryEmail'),
|
||||
@@ -162,7 +163,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="GoogleAccount"
|
||||
name="googleAccount"
|
||||
component={GoogleAccountView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryGoogle'),
|
||||
@@ -171,7 +172,7 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
|
||||
}}
|
||||
/>
|
||||
<TabRow.Screen
|
||||
name="PianoSettings"
|
||||
name="pianoSettings"
|
||||
component={PianoSettingsView}
|
||||
options={{
|
||||
title: translate('SettingsCategoryPiano'),
|
||||
|
||||
Reference in New Issue
Block a user