Merge branch 'feature/adc/artist-view' into feature/adc/#224-genre-view
This commit is contained in:
485
front/API.ts
485
front/API.ts
@@ -1,21 +1,26 @@
|
||||
import Artist from './models/Artist';
|
||||
import Artist, { ArtistHandler } from './models/Artist';
|
||||
import Album from './models/Album';
|
||||
import Chapter from './models/Chapter';
|
||||
import Lesson from './models/Lesson';
|
||||
import Genre from './models/Genre';
|
||||
import Genre, { GenreHandler } from './models/Genre';
|
||||
import LessonHistory from './models/LessonHistory';
|
||||
import Song from './models/Song';
|
||||
import SongHistory from './models/SongHistory';
|
||||
import User from './models/User';
|
||||
import Song, { SongHandler } from './models/Song';
|
||||
import { SongHistoryHandler, SongHistoryItem, SongHistoryItemHandler } from './models/SongHistory';
|
||||
import User, { UserHandler } from './models/User';
|
||||
import Constants from 'expo-constants';
|
||||
import store from './state/Store';
|
||||
import { Platform } from 'react-native';
|
||||
import { en } from './i18n/Translations';
|
||||
import UserSettings from './models/UserSettings';
|
||||
import { PartialDeep } from 'type-fest';
|
||||
import SearchHistory from './models/SearchHistory';
|
||||
import UserSettings, { UserSettingsHandler } from './models/UserSettings';
|
||||
import { PartialDeep, RequireExactlyOne } from 'type-fest';
|
||||
import SearchHistory, { SearchHistoryHandler } from './models/SearchHistory';
|
||||
import { Query } from './Queries';
|
||||
import CompetenciesTable from './components/CompetenciesTable';
|
||||
import ResponseHandler from './models/ResponseHandler';
|
||||
import { PlageHandler } from './models/Plage';
|
||||
import { ListHandler } from './models/List';
|
||||
import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
|
||||
import * as yup from 'yup';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
type RegistrationInput = AuthenticationInput & { email: string };
|
||||
@@ -26,10 +31,14 @@ type FetchParams = {
|
||||
route: string;
|
||||
body?: object;
|
||||
method?: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT';
|
||||
// If true, No JSON parsing is done, the raw response's content is returned
|
||||
raw?: true;
|
||||
};
|
||||
|
||||
type HandleParams<APIType = unknown, ModelType = APIType> = RequireExactlyOne<{
|
||||
raw: true;
|
||||
emptyResponse: true;
|
||||
handler: ResponseHandler<APIType, ModelType>;
|
||||
}>;
|
||||
|
||||
// This Exception is intended to cover all business logic errors (invalid credentials, couldn't find a song, etc.)
|
||||
// technical errors (network, server, etc.) should be handled as standard Error exceptions
|
||||
// it helps to filter errors in the catch block, APIErrors messages should
|
||||
@@ -46,37 +55,66 @@ export class APIError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// we will need the same thing for the scorometer API url
|
||||
const baseAPIUrl =
|
||||
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
|
||||
? '/api'
|
||||
: Constants.manifest?.extra?.apiUrl;
|
||||
export class ValidationError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
export default class API {
|
||||
public static async fetch(params: FetchParams) {
|
||||
public static readonly baseUrl =
|
||||
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
|
||||
? '/api'
|
||||
: Constants.manifest?.extra?.apiUrl;
|
||||
public static async fetch(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams>, 'raw'>
|
||||
): Promise<ArrayBuffer>;
|
||||
public static async fetch(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams>, 'emptyResponse'>
|
||||
): Promise<void>;
|
||||
public static async fetch<A, R>(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams<A, R>>, 'handler'>
|
||||
): Promise<R>;
|
||||
public static async fetch(params: FetchParams): Promise<void>;
|
||||
public static async fetch(params: FetchParams, handle?: HandleParams) {
|
||||
const jwtToken = store.getState().user.accessToken;
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const response = await fetch(`${baseAPIUrl}${params.route}`, {
|
||||
const response = await fetch(`${API.baseUrl}${params.route}`, {
|
||||
headers: (jwtToken && { ...header, Authorization: `Bearer ${jwtToken}` }) || header,
|
||||
body: JSON.stringify(params.body),
|
||||
method: params.method ?? 'GET',
|
||||
}).catch(() => {
|
||||
throw new Error('Error while fetching API: ' + baseAPIUrl);
|
||||
throw new Error('Error while fetching API: ' + API.baseUrl);
|
||||
});
|
||||
if (params.raw) {
|
||||
if (!handle || handle.emptyResponse) {
|
||||
return;
|
||||
}
|
||||
if (handle.raw) {
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
const handler = handle.handler;
|
||||
const body = await response.text();
|
||||
try {
|
||||
const jsonResponse = body.length != 0 ? JSON.parse(body) : {};
|
||||
const jsonResponse = JSON.parse(body);
|
||||
if (!response.ok) {
|
||||
throw new APIError(response.statusText ?? body, response.status);
|
||||
}
|
||||
return jsonResponse;
|
||||
const validated = await handler.validator.validate(jsonResponse).catch((e) => {
|
||||
if (e instanceof yup.ValidationError) {
|
||||
console.error(e, 'Got: ' + body);
|
||||
throw new ValidationError(e.message);
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
return handler.transformer(handler.validator.cast(validated));
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) throw new Error("Error while parsing Server's response");
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -84,17 +122,21 @@ export default class API {
|
||||
public static async authenticate(
|
||||
authenticationInput: AuthenticationInput
|
||||
): Promise<AccessToken> {
|
||||
return API.fetch({
|
||||
route: '/auth/login',
|
||||
body: authenticationInput,
|
||||
method: 'POST',
|
||||
})
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/login',
|
||||
body: authenticationInput,
|
||||
method: 'POST',
|
||||
},
|
||||
{ handler: AccessTokenResponseHandler }
|
||||
)
|
||||
.then((responseBody) => responseBody.access_token)
|
||||
.catch((e) => {
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
|
||||
/// If validation fails, it means that auth failed.
|
||||
/// We want that 401 error to be thrown, instead of the plain validation vone
|
||||
if (e.status == 401)
|
||||
throw new APIError('invalidCredentials', 401, 'invalidCredentials');
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
@@ -118,12 +160,20 @@ export default class API {
|
||||
}
|
||||
|
||||
public static async createAndGetGuestAccount(): Promise<AccessToken> {
|
||||
const response = await API.fetch({
|
||||
route: '/auth/guest',
|
||||
method: 'POST',
|
||||
});
|
||||
if (!response.access_token) throw new APIError('No access token', response.status);
|
||||
return response.access_token;
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/guest',
|
||||
method: 'POST',
|
||||
},
|
||||
{ handler: AccessTokenResponseHandler }
|
||||
)
|
||||
.then(({ access_token }) => access_token)
|
||||
.catch((e) => {
|
||||
if (e.status == 401)
|
||||
throw new APIError('invalidCredentials', 401, 'invalidCredentials');
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
public static async transformGuestToUser(registrationInput: RegistrationInput): Promise<void> {
|
||||
@@ -140,50 +190,28 @@ export default class API {
|
||||
public static getUserInfo(): Query<User> {
|
||||
return {
|
||||
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',
|
||||
exec: async () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/auth/me',
|
||||
},
|
||||
} as User;
|
||||
},
|
||||
{ handler: UserHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
public static getUserSettings(): Query<UserSettings> {
|
||||
return {
|
||||
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,
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/auth/me/settings',
|
||||
},
|
||||
recommendations: settings.recommendations,
|
||||
weeklyReport: settings.weeklyReport,
|
||||
leaderBoard: settings.leaderBoard,
|
||||
showActivity: settings.showActivity,
|
||||
};
|
||||
},
|
||||
{
|
||||
handler: UserSettingsHandler,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -222,28 +250,15 @@ export default class API {
|
||||
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)
|
||||
);
|
||||
},
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: '/song',
|
||||
},
|
||||
{
|
||||
handler: PlageHandler(SongHandler),
|
||||
}
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -254,55 +269,31 @@ export default class API {
|
||||
public static getSong(songId: number): Query<Song> {
|
||||
return {
|
||||
key: ['song', songId],
|
||||
exec: async () => {
|
||||
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;
|
||||
},
|
||||
exec: async () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}`,
|
||||
},
|
||||
{ handler: SongHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* @description retrieves songs from a specific artist
|
||||
* @param artistId is the id of the artist that composed the songs aimed
|
||||
* @returns a Promise of Songs type array
|
||||
*/
|
||||
public static getSongsByArtist(artistId: string): Query<Song[]> {
|
||||
public static getSongsByArtist(artistId: number): Query<Song[]> {
|
||||
return {
|
||||
key: ['songs', artistId],
|
||||
exec: async () => {
|
||||
const songs = await API.fetch({
|
||||
route: `/song/artist/${artistId}`,
|
||||
});
|
||||
|
||||
// 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)
|
||||
);
|
||||
},
|
||||
key: ['artist', artistId, 'songs'],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song?artistId=${artistId}`,
|
||||
},
|
||||
{ handler: PlageHandler(SongHandler) }
|
||||
).then(({ data }) => data),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -314,10 +305,14 @@ export default class API {
|
||||
return {
|
||||
key: ['midi', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/midi`,
|
||||
raw: true,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/midi`,
|
||||
},
|
||||
{
|
||||
raw: true,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -326,7 +321,7 @@ export default class API {
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getArtistIllustration(artistId: number): string {
|
||||
return `${baseAPIUrl}/artist/${artistId}/illustration`;
|
||||
return `${API.baseUrl}/artist/${artistId}/illustration`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,18 +329,18 @@ export default class API {
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getGenreIllustration(genreId: number): string {
|
||||
return `${baseAPIUrl}/genre/${genreId}/illustration`;
|
||||
return `${API.baseUrl}/genre/${genreId}/illustration`;
|
||||
}
|
||||
|
||||
public static getGenre(genreId: number): Query<Genre> {
|
||||
return {
|
||||
key: ['genre', genreId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/genre/${genreId}`,
|
||||
}),
|
||||
}
|
||||
}
|
||||
// public static getGenre(genreId: number): Query<Genre> {
|
||||
// return {
|
||||
// key: ['genre', genreId],
|
||||
// exec: () =>
|
||||
// API.fetch({
|
||||
// route: `/genre/${genreId}`,
|
||||
// }),
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Retrive a song's musicXML partition
|
||||
@@ -355,10 +350,12 @@ export default class API {
|
||||
return {
|
||||
key: ['musixml', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/musicXml`,
|
||||
raw: true,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/musicXml`,
|
||||
},
|
||||
{ raw: true }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -369,9 +366,12 @@ export default class API {
|
||||
return {
|
||||
key: ['artist', artistId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/artist/${artistId}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/artist/${artistId}`,
|
||||
},
|
||||
{ handler: ArtistHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -400,13 +400,16 @@ export default class API {
|
||||
* Retrieve a song's play history
|
||||
* @param songId the id to find the song
|
||||
*/
|
||||
public static getSongHistory(songId: number): Query<{ best: number; history: SongHistory[] }> {
|
||||
public static getSongHistory(songId: number) {
|
||||
return {
|
||||
key: ['song', 'history', songId],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/song/${songId}/history`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/history`,
|
||||
},
|
||||
{ handler: SongHistoryHandler }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -418,9 +421,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'song', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/songs/${query}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/songs/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -432,9 +438,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'artist', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/artists/${query}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/artists/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(ArtistHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -442,31 +451,27 @@ export default class API {
|
||||
* Search Album by name
|
||||
* @param query the string used to find the album
|
||||
*/
|
||||
public static searchAlbum(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
query: string
|
||||
): Query<Album[]> {
|
||||
public static searchAlbum(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[],
|
||||
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',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -477,9 +482,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'genre', query],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/search/genres/${query}`,
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/genres/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(GenreHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -491,7 +499,7 @@ export default class API {
|
||||
return {
|
||||
key: ['lesson', lessonId],
|
||||
exec: async () => ({
|
||||
title: 'Song',
|
||||
name: 'Song',
|
||||
description: 'A song',
|
||||
requiredLevel: 1,
|
||||
mainSkill: 'lead-head-change',
|
||||
@@ -500,15 +508,15 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
public static getFavorites(): Query<Song[]> {
|
||||
return {
|
||||
key: 'favorites',
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: '/search/songs/o',
|
||||
}),
|
||||
};
|
||||
}
|
||||
// public static getFavorites(): Query<Song[]> {
|
||||
// return {
|
||||
// key: 'favorites',
|
||||
// exec: () =>
|
||||
// API.fetch({
|
||||
// route: '/search/songs/o',
|
||||
// }),
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Retrieve the authenticated user's search history
|
||||
@@ -520,22 +528,12 @@ export default class API {
|
||||
return {
|
||||
key: ['search', 'history', 'skip', skip, 'take', take],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`,
|
||||
method: 'GET',
|
||||
}).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)
|
||||
)
|
||||
API.fetch(
|
||||
{
|
||||
route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`,
|
||||
method: 'GET',
|
||||
},
|
||||
{ handler: ListHandler(SearchHistoryHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -570,13 +568,16 @@ export default class API {
|
||||
* Retrieve the authenticated user's play history
|
||||
* * @returns an array of songs
|
||||
*/
|
||||
public static getUserPlayHistory(): Query<SongHistory[]> {
|
||||
public static getUserPlayHistory(): Query<SongHistoryItem[]> {
|
||||
return {
|
||||
key: ['history'],
|
||||
exec: () =>
|
||||
API.fetch({
|
||||
route: '/history',
|
||||
}),
|
||||
API.fetch(
|
||||
{
|
||||
route: '/history',
|
||||
},
|
||||
{ handler: ListHandler(SongHistoryItemHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -597,36 +598,32 @@ export default class API {
|
||||
}
|
||||
|
||||
public static async updateUserEmail(newEmail: string): Promise<User> {
|
||||
const rep = await API.fetch({
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
email: newEmail,
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
email: newEmail,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (rep.error) {
|
||||
throw new Error(rep.error);
|
||||
}
|
||||
return rep;
|
||||
{ handler: UserHandler }
|
||||
);
|
||||
}
|
||||
|
||||
public static async updateUserPassword(
|
||||
oldPassword: string,
|
||||
newPassword: string
|
||||
): Promise<User> {
|
||||
const rep = await API.fetch({
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
oldPassword: oldPassword,
|
||||
password: newPassword,
|
||||
return API.fetch(
|
||||
{
|
||||
route: '/auth/me',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
oldPassword: oldPassword,
|
||||
password: newPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (rep.error) {
|
||||
throw new Error(rep.error);
|
||||
}
|
||||
return rep;
|
||||
{ handler: UserHandler }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { useColorScheme } from "react-native";
|
||||
import { RootState, useSelector } from "../state/Store";
|
||||
import { Box, Pressable } from "native-base";
|
||||
import { useColorScheme } from 'react-native';
|
||||
import { RootState, useSelector } from '../state/Store';
|
||||
import { Box, Pressable } from 'native-base';
|
||||
|
||||
const RowCustom = (
|
||||
props: Parameters<typeof Box>[0] & { onPress?: () => void }
|
||||
) => {
|
||||
const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
|
||||
const settings = useSelector((state: RootState) => state.settings.local);
|
||||
const systemColorMode = useColorScheme();
|
||||
const colorScheme = settings.colorScheme;
|
||||
@@ -17,13 +15,13 @@ const RowCustom = (
|
||||
py={3}
|
||||
my={1}
|
||||
bg={
|
||||
(colorScheme == "system" ? systemColorMode : colorScheme) == "dark"
|
||||
(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
|
||||
? isHovered || isPressed
|
||||
? "gray.800"
|
||||
? 'gray.800'
|
||||
: undefined
|
||||
: isHovered || isPressed
|
||||
? "coolGray.200"
|
||||
: undefined
|
||||
? 'coolGray.200'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{props.children}
|
||||
@@ -33,4 +31,4 @@ const RowCustom = (
|
||||
);
|
||||
};
|
||||
|
||||
export default RowCustom;
|
||||
export default RowCustom;
|
||||
|
||||
@@ -5,7 +5,6 @@ import TextButton from "./TextButton";
|
||||
import { MaterialIcons } from "@expo/vector-icons";
|
||||
import API from "../API";
|
||||
|
||||
|
||||
type SongRowProps = {
|
||||
liked: boolean;
|
||||
song: Song | SongWithArtist; // TODO: remove Song
|
||||
@@ -17,8 +16,8 @@ const handleLikeButton = {
|
||||
|
||||
const SongRow = ({ song, onPress, liked }: SongRowProps) => {
|
||||
return (
|
||||
<RowCustom width={"100%"}>
|
||||
<HStack px={2} space={5} justifyContent={"space-between"}>
|
||||
<RowCustom width={'100%'}>
|
||||
<HStack px={2} space={5} justifyContent={'space-between'}>
|
||||
<Image
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
@@ -35,11 +34,11 @@ const SongRow = ({ song, onPress, liked }: SongRowProps) => {
|
||||
}} />
|
||||
<HStack
|
||||
style={{
|
||||
display: "flex",
|
||||
display: 'flex',
|
||||
flexShrink: 1,
|
||||
flexGrow: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
space={6}
|
||||
>
|
||||
@@ -49,7 +48,7 @@ const SongRow = ({ song, onPress, liked }: SongRowProps) => {
|
||||
}}
|
||||
isTruncated
|
||||
pl={5}
|
||||
maxW={"100%"}
|
||||
maxW={'100%'}
|
||||
bold
|
||||
fontSize="md"
|
||||
>
|
||||
@@ -59,17 +58,17 @@ const SongRow = ({ song, onPress, liked }: SongRowProps) => {
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
}}
|
||||
fontSize={"sm"}
|
||||
fontSize={'sm'}
|
||||
>
|
||||
{song.artistId ?? "artist"}
|
||||
{song.artistId ?? 'artist'}
|
||||
</Text>
|
||||
</HStack>
|
||||
<TextButton
|
||||
flexShrink={0}
|
||||
flexGrow={0}
|
||||
translate={{ translationKey: "playBtn" }}
|
||||
translate={{ translationKey: 'playBtn' }}
|
||||
colorScheme="primary"
|
||||
variant={"outline"}
|
||||
variant={'outline'}
|
||||
size="sm"
|
||||
mr={5}
|
||||
onPress={onPress}
|
||||
@@ -79,4 +78,4 @@ const SongRow = ({ song, onPress, liked }: SongRowProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default SongRow;
|
||||
export default SongRow;
|
||||
|
||||
13
front/models/AccessTokenResponse.ts
Normal file
13
front/models/AccessTokenResponse.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
const AccessTokenResponseValidator = yup.object({
|
||||
access_token: yup.string().required(),
|
||||
});
|
||||
|
||||
type AccessTokenResponse = yup.InferType<typeof AccessTokenResponseValidator>;
|
||||
|
||||
export const AccessTokenResponseHandler: ResponseHandler<AccessTokenResponse> = {
|
||||
validator: AccessTokenResponseValidator,
|
||||
transformer: (value) => value,
|
||||
};
|
||||
@@ -1,4 +1,12 @@
|
||||
import Model from './Model';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const AlbumValidator = yup
|
||||
.object({
|
||||
name: yup.string().required(),
|
||||
artistId: yup.number().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
interface Album extends Model {
|
||||
name: string;
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import Model from './Model';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const ArtistValidator = yup
|
||||
.object({
|
||||
name: yup.string().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const ArtistHandler: ResponseHandler<Artist> = {
|
||||
validator: ArtistValidator,
|
||||
transformer: (value) => value,
|
||||
};
|
||||
|
||||
interface Artist extends Model {
|
||||
name: string;
|
||||
picture?: string;
|
||||
}
|
||||
|
||||
export default Artist;
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import Model from './Model';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const GenreValidator = yup
|
||||
.object({
|
||||
name: yup.string().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const GenreHandler: ResponseHandler<Genre> = {
|
||||
validator: GenreValidator,
|
||||
transformer: (value) => value,
|
||||
};
|
||||
|
||||
interface Genre extends Model {
|
||||
name: string;
|
||||
|
||||
@@ -1,26 +1,19 @@
|
||||
import Skill from './Skill';
|
||||
import Model from './Model';
|
||||
import { SkillValidator } from './Skill';
|
||||
import { ModelValidator } from './Model';
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const LessonValidator = yup
|
||||
.object({
|
||||
name: yup.string().required(),
|
||||
description: yup.string().required(),
|
||||
requiredLevel: yup.number().required(),
|
||||
mainSkill: SkillValidator.required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
/**
|
||||
* A Lesson is an exercice that the user can try to practice a skill
|
||||
*/
|
||||
interface Lesson extends Model {
|
||||
/**
|
||||
* The title of the lesson
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* Short description of the lesson
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* The minimum level required for the user to access this lesson
|
||||
*/
|
||||
requiredLevel: number;
|
||||
/**
|
||||
* The main skill learnt in this lesson
|
||||
*/
|
||||
mainSkill: Skill;
|
||||
}
|
||||
type Lesson = yup.InferType<typeof LessonValidator>;
|
||||
|
||||
export default Lesson;
|
||||
|
||||
11
front/models/List.ts
Normal file
11
front/models/List.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
const ListValidator = <T>(itemType: yup.Schema<T>) => yup.array(itemType).required();
|
||||
|
||||
export const ListHandler = <A, R>(
|
||||
itemHandler: ResponseHandler<A, R>
|
||||
): ResponseHandler<A[], R[]> => ({
|
||||
validator: ListValidator(itemHandler.validator),
|
||||
transformer: (plage) => plage.map((item) => itemHandler.transformer(item)),
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
interface Model {
|
||||
id: number;
|
||||
}
|
||||
import * as yup from 'yup';
|
||||
|
||||
export const ModelValidator = yup.object({
|
||||
id: yup.number().required(),
|
||||
});
|
||||
|
||||
type Model = yup.InferType<typeof ModelValidator>;
|
||||
|
||||
export default Model;
|
||||
|
||||
34
front/models/Plage.ts
Normal file
34
front/models/Plage.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
// Ty https://github.com/Arthi-chaud/Meelo/blob/master/front/src/models/pagination.ts
|
||||
export const PlageValidator = <T>(itemType: yup.Schema<T>) =>
|
||||
yup.object({
|
||||
data: yup.array(itemType).required(),
|
||||
metadata: yup.object({
|
||||
/**
|
||||
* Current route
|
||||
*/
|
||||
this: yup.string().required(),
|
||||
/**
|
||||
* route to use for the next items
|
||||
*/
|
||||
next: yup.string().required().nullable(),
|
||||
/**
|
||||
* route to use for the previous items
|
||||
*/
|
||||
previous: yup.string().required().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
type Plage<T> = yup.InferType<ReturnType<typeof PlageValidator<T>>>;
|
||||
|
||||
export const PlageHandler = <A, R>(
|
||||
itemHandler: ResponseHandler<A, R>
|
||||
): ResponseHandler<Plage<A>, Plage<R>> => ({
|
||||
validator: PlageValidator(itemHandler.validator),
|
||||
transformer: (plage) => ({
|
||||
...plage,
|
||||
data: plage.data.map((item) => itemHandler.transformer(item)),
|
||||
}),
|
||||
});
|
||||
8
front/models/ResponseHandler.ts
Normal file
8
front/models/ResponseHandler.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as yup from 'yup';
|
||||
|
||||
type ResponseHandler<APIType, ModelType = APIType> = {
|
||||
validator: yup.Schema<APIType>;
|
||||
transformer: (value: APIType) => ModelType;
|
||||
};
|
||||
|
||||
export default ResponseHandler;
|
||||
@@ -1,4 +1,29 @@
|
||||
import Model from './Model';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const SearchType = ['song', 'artist', 'album'] as const;
|
||||
export type SearchType = (typeof SearchType)[number];
|
||||
|
||||
const SearchHistoryValidator = yup
|
||||
.object({
|
||||
query: yup.string().required(),
|
||||
type: yup.mixed<SearchType>().oneOf(SearchType).required(),
|
||||
userId: yup.number().required(),
|
||||
searchDate: yup.date().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const SearchHistoryHandler: ResponseHandler<
|
||||
yup.InferType<typeof SearchHistoryValidator>,
|
||||
SearchHistory
|
||||
> = {
|
||||
validator: SearchHistoryValidator,
|
||||
transformer: (value) => ({
|
||||
...value,
|
||||
timestamp: value.searchDate,
|
||||
}),
|
||||
};
|
||||
|
||||
interface SearchHistory extends Model {
|
||||
query: string;
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
type Skill =
|
||||
| 'rhythm'
|
||||
| 'two-hands'
|
||||
| 'combos'
|
||||
| 'arpeggio'
|
||||
| 'distance'
|
||||
| 'left-hand'
|
||||
| 'right-hand'
|
||||
| 'lead-head-change'
|
||||
| 'chord-complexity'
|
||||
| 'chord-timing'
|
||||
| 'pedal'
|
||||
| 'precision';
|
||||
import * as yup from 'yup';
|
||||
|
||||
const Skills = [
|
||||
'rhythm',
|
||||
'two-hands',
|
||||
'combos',
|
||||
'arpeggio',
|
||||
'distance',
|
||||
'left-hand',
|
||||
'right-hand',
|
||||
'lead-head-change',
|
||||
'chord-complexity',
|
||||
'chord-timing',
|
||||
'pedal',
|
||||
'precision',
|
||||
] as const;
|
||||
type Skill = (typeof Skills)[number];
|
||||
|
||||
export const SkillValidator = yup.mixed<Skill>().oneOf(Skills);
|
||||
|
||||
export default Skill;
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
import Model from './Model';
|
||||
import SongDetails from './SongDetails';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
import SongDetails, { SongDetailsHandler, SongDetailsValidator } from './SongDetails';
|
||||
import Artist from './Artist';
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
import API from '../API';
|
||||
|
||||
export const SongValidator = yup
|
||||
.object({
|
||||
name: yup.string().required(),
|
||||
midiPath: yup.string().required(),
|
||||
musicXmlPath: yup.string().required(),
|
||||
artistId: yup.number().required(),
|
||||
albumId: yup.number().required().nullable(),
|
||||
genreId: yup.number().required().nullable(),
|
||||
difficulties: SongDetailsValidator.required(),
|
||||
illustrationPath: yup.string().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const SongHandler: ResponseHandler<yup.InferType<typeof SongValidator>, Song> = {
|
||||
validator: SongValidator,
|
||||
transformer: (song) => ({
|
||||
id: song.id,
|
||||
name: song.name,
|
||||
artistId: song.artistId,
|
||||
albumId: song.albumId,
|
||||
genreId: song.genreId,
|
||||
details: SongDetailsHandler.transformer(song.difficulties),
|
||||
cover: `${API.baseUrl}/song/${song.id}/illustration`,
|
||||
}),
|
||||
};
|
||||
|
||||
interface Song extends Model {
|
||||
id: number;
|
||||
|
||||
@@ -1,7 +1,34 @@
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const SongDetailsValidator = yup.object({
|
||||
length: yup.number().required(),
|
||||
rhythm: yup.number().required(),
|
||||
arpeggio: yup.number().required(),
|
||||
distance: yup.number().required(),
|
||||
lefthand: yup.number().required(),
|
||||
twohands: yup.number().required(),
|
||||
notecombo: yup.number().required(),
|
||||
precision: yup.number().required(),
|
||||
righthand: yup.number().required(),
|
||||
pedalpoint: yup.number().required(),
|
||||
chordtiming: yup.number().required(),
|
||||
leadhandchange: yup.number().required(),
|
||||
chordcomplexity: yup.number().required(),
|
||||
});
|
||||
|
||||
export const SongDetailsHandler: ResponseHandler<
|
||||
yup.InferType<typeof SongDetailsValidator>,
|
||||
SongDetails
|
||||
> = {
|
||||
validator: SongDetailsValidator,
|
||||
transformer: (value) => value,
|
||||
};
|
||||
|
||||
interface SongDetails {
|
||||
length: number;
|
||||
rhythm: number;
|
||||
arppegio: number;
|
||||
arpeggio: number;
|
||||
distance: number;
|
||||
lefthand: number;
|
||||
righthand: number;
|
||||
@@ -10,7 +37,7 @@ interface SongDetails {
|
||||
precision: number;
|
||||
pedalpoint: number;
|
||||
chordtiming: number;
|
||||
leadheadchange: number;
|
||||
leadhandchange: number;
|
||||
chordcomplexity: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,44 @@
|
||||
interface SongHistory {
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const SongHistoryItemValidator = yup.object({
|
||||
songID: yup.number().required(),
|
||||
userID: yup.number().required(),
|
||||
score: yup.number().required(),
|
||||
difficulties: yup.mixed().required(),
|
||||
});
|
||||
|
||||
export const SongHistoryItemHandler: ResponseHandler<
|
||||
yup.InferType<typeof SongHistoryItemValidator>,
|
||||
SongHistoryItem
|
||||
> = {
|
||||
validator: SongHistoryItemValidator,
|
||||
transformer: (value) => ({
|
||||
...value,
|
||||
difficulties: value.difficulties,
|
||||
}),
|
||||
};
|
||||
|
||||
export const SongHistoryValidator = yup.object({
|
||||
best: yup.number().required().nullable(),
|
||||
history: yup.array(SongHistoryItemValidator).required(),
|
||||
});
|
||||
|
||||
export type SongHistory = yup.InferType<typeof SongHistoryValidator>;
|
||||
|
||||
export const SongHistoryHandler: ResponseHandler<SongHistory> = {
|
||||
validator: SongHistoryValidator,
|
||||
transformer: (value) => ({
|
||||
...value,
|
||||
history: value.history.map((item) => SongHistoryItemHandler.transformer(item)),
|
||||
}),
|
||||
};
|
||||
|
||||
export type SongHistoryItem = {
|
||||
songID: number;
|
||||
userID: number;
|
||||
score: number;
|
||||
difficulties: JSON;
|
||||
}
|
||||
difficulties: object;
|
||||
};
|
||||
|
||||
export default SongHistory;
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
import UserData from './UserData';
|
||||
import Model from './Model';
|
||||
import UserSettings from './UserSettings';
|
||||
import Model, { ModelValidator } from './Model';
|
||||
import * as yup from 'yup';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const UserValidator = yup
|
||||
.object({
|
||||
username: yup.string().required(),
|
||||
password: yup.string().required(),
|
||||
email: yup.string().required(),
|
||||
isGuest: yup.boolean().required(),
|
||||
partyPlayed: yup.number().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, User> = {
|
||||
validator: UserValidator,
|
||||
transformer: (value) => ({
|
||||
...value,
|
||||
name: value.username,
|
||||
premium: false,
|
||||
data: {
|
||||
gamesPlayed: value.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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
interface User extends Model {
|
||||
name: string;
|
||||
@@ -8,7 +33,13 @@ interface User extends Model {
|
||||
isGuest: boolean;
|
||||
premium: boolean;
|
||||
data: UserData;
|
||||
settings: UserSettings;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
gamesPlayed: number;
|
||||
xp: number;
|
||||
avatar: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default User;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
interface UserData {
|
||||
gamesPlayed: number;
|
||||
xp: number;
|
||||
avatar: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default UserData;
|
||||
@@ -1,3 +1,40 @@
|
||||
import * as yup from 'yup';
|
||||
import { ModelValidator } from './Model';
|
||||
import ResponseHandler from './ResponseHandler';
|
||||
|
||||
export const UserSettingsValidator = yup
|
||||
.object({
|
||||
userId: yup.number().required(),
|
||||
pushNotification: yup.boolean().required(),
|
||||
emailNotification: yup.boolean().required(),
|
||||
trainingNotification: yup.boolean().required(),
|
||||
newSongNotification: yup.boolean().required(),
|
||||
recommendations: yup.boolean().required(),
|
||||
weeklyReport: yup.boolean().required(),
|
||||
leaderBoard: yup.boolean().required(),
|
||||
showActivity: yup.boolean().required(),
|
||||
})
|
||||
.concat(ModelValidator);
|
||||
|
||||
export const UserSettingsHandler: ResponseHandler<
|
||||
yup.InferType<typeof UserSettingsValidator>,
|
||||
UserSettings
|
||||
> = {
|
||||
validator: UserSettingsValidator,
|
||||
transformer: (settings) => ({
|
||||
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,
|
||||
}),
|
||||
};
|
||||
|
||||
interface UserSettings {
|
||||
notifications: {
|
||||
pushNotif: boolean;
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"soundfont-player": "^0.12.0",
|
||||
"standardized-audio-context": "^25.3.51",
|
||||
"type-fest": "^3.6.0",
|
||||
"yup": "^0.32.11"
|
||||
"yup": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19.3",
|
||||
|
||||
@@ -1,139 +1,34 @@
|
||||
import { VStack, Text, Box, Image, Heading, IconButton, Icon, Container, Center, useBreakpointValue, ScrollView } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
// import { Box, Image, Heading, useBreakpointValue } from 'native-base';
|
||||
import { SafeAreaView } from 'react-native';
|
||||
import { useQuery } from '../Queries';
|
||||
import { LoadingView } from '../components/Loading';
|
||||
import API from '../API';
|
||||
import Song, { SongWithArtist } from '../models/Song';
|
||||
import SongRow from '../components/SongRow';
|
||||
import { Key, useEffect, useState } from 'react';
|
||||
import { useNavigation } from '../Navigation';
|
||||
import { Key } from 'react';
|
||||
import { RouteProps, useNavigation } from '../Navigation';
|
||||
import { ImageBackground } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
const colorRange = [
|
||||
{
|
||||
code: '#364fc7',
|
||||
},
|
||||
{
|
||||
code: '#5c940d',
|
||||
},
|
||||
{
|
||||
code: '#c92a2a',
|
||||
},
|
||||
{
|
||||
code: '#d6336c',
|
||||
},
|
||||
{
|
||||
code: '#20c997'
|
||||
}
|
||||
]
|
||||
|
||||
const songs: Song[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Dancing Queen",
|
||||
artistId: 1,
|
||||
albumId: 1,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Mamma Mia",
|
||||
artistId: 1,
|
||||
albumId: 1,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Take a Chance on Me",
|
||||
artistId: 1,
|
||||
albumId: 2,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "Fernando",
|
||||
artistId: 1,
|
||||
albumId: 3,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Waterloo",
|
||||
artistId: 1,
|
||||
albumId: 4,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: "The Winner Takes It All",
|
||||
artistId: 1,
|
||||
albumId: 5,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: "SOS",
|
||||
artistId: 1,
|
||||
albumId: 6,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: "Knowing Me, Knowing You",
|
||||
artistId: 1,
|
||||
albumId: 7,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: "Money, Money, Money",
|
||||
artistId: 1,
|
||||
albumId: 8,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: "Gimme! Gimme! Gimme! (A Man After Midnight)",
|
||||
artistId: 1,
|
||||
albumId: 9,
|
||||
genreId: 1,
|
||||
cover: undefined,
|
||||
details: undefined,
|
||||
},
|
||||
];
|
||||
type ArtistDetailsViewProps = {
|
||||
artistId: number;
|
||||
};
|
||||
|
||||
const ArtistDetailsView = ({ artistId }: any) => {
|
||||
const { isLoading: isLoadingArt, data: artistData, error: isErrorArt } = useQuery(API.getArtist(artistId));
|
||||
const { isLoading: isLoadingSong, data: songData = [], error: isErrorSong } = useQuery(API.getSongsByArtist(artistId));
|
||||
const screenSize = useBreakpointValue({ base: "small", md: "big" });
|
||||
const isMobileView = screenSize == "small";
|
||||
const ArtistDetailsView = ({ artistId }: RouteProps<ArtistDetailsViewProps>) => {
|
||||
const artistQuery = useQuery(API.getArtist(artistId));
|
||||
const songsQuery = useQuery(API.getSongsByArtist(artistId));
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isMobileView = screenSize == 'small';
|
||||
const navigation = useNavigation();
|
||||
|
||||
if (isLoadingArt) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
if (isErrorArt) {
|
||||
if (artistQuery.isError || songsQuery.isError) {
|
||||
navigation.navigate('Error');
|
||||
return <></>;
|
||||
}
|
||||
if (!artistQuery.data || songsQuery.data === undefined) {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -146,19 +41,21 @@ const ArtistDetailsView = ({ artistId }: any) => {
|
||||
style={{height : '100%', width : '100%'}}/>
|
||||
</ImageBackground>
|
||||
<Box>
|
||||
<Heading mt={-20} ml={3} fontSize={50}>{artistData?.name}</Heading>
|
||||
<Heading mt={-20} ml={3} fontSize={50}>{artistQuery.data.name}</Heading>
|
||||
<ScrollView mt={3}>
|
||||
{songs.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>
|
||||
{songsQuery.data.map((comp: Song, index: Key | null | undefined) => (
|
||||
<SongRow
|
||||
key={index}
|
||||
song={comp}
|
||||
liked={true}
|
||||
onPress={() => {
|
||||
API.createSearchHistoryEntry(comp.name, 'song');
|
||||
navigation.navigate('Song', { songId: comp.id });
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</ScrollView>
|
||||
</Box>
|
||||
</ScrollView>
|
||||
|
||||
@@ -32,7 +32,7 @@ import PartitionView from '../components/PartitionView';
|
||||
import TextButton from '../components/TextButton';
|
||||
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
|
||||
import * as Linking from 'expo-linking';
|
||||
import { URL } from 'url';
|
||||
import url from 'url';
|
||||
|
||||
type PlayViewProps = {
|
||||
songId: number;
|
||||
@@ -48,9 +48,9 @@ type ScoreMessage = {
|
||||
let scoroBaseApiUrl = Constants.manifest?.extra?.scoroUrl;
|
||||
|
||||
if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') {
|
||||
Linking.getInitialURL().then((url) => {
|
||||
if (url !== null) {
|
||||
const location = new URL(url);
|
||||
Linking.getInitialURL().then((initUrl) => {
|
||||
if (initUrl !== null) {
|
||||
const location = url.parse(initUrl);
|
||||
if (location.protocol === 'https:') {
|
||||
scoroBaseApiUrl = 'wss://' + location.host + '/ws';
|
||||
} else {
|
||||
|
||||
@@ -1232,7 +1232,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.6", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.20.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b"
|
||||
integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==
|
||||
@@ -4743,7 +4743,7 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash@^4.14.167", "@types/lodash@^4.14.175":
|
||||
"@types/lodash@^4.14.167":
|
||||
version "4.14.191"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
||||
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
|
||||
@@ -12596,11 +12596,6 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
|
||||
lodash.clone@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
|
||||
@@ -13710,11 +13705,6 @@ nano-time@1.0.0:
|
||||
dependencies:
|
||||
big-integer "^1.6.16"
|
||||
|
||||
nanoclone@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoclone/-/nanoclone-0.2.1.tgz#dd4090f8f1a110d26bb32c49ed2f5b9235209ed4"
|
||||
integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA==
|
||||
|
||||
nanoid@^3.1.23, nanoid@^3.3.1:
|
||||
version "3.3.4"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
|
||||
@@ -15428,7 +15418,7 @@ prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
|
||||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
property-expr@^2.0.4:
|
||||
property-expr@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-2.0.5.tgz#278bdb15308ae16af3e3b9640024524f4dc02cb4"
|
||||
integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA==
|
||||
@@ -17936,6 +17926,11 @@ timsort@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==
|
||||
|
||||
tiny-case@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03"
|
||||
integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==
|
||||
|
||||
tinycolor2@^1.4.2:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.5.2.tgz#7d30b4584d8b7d62b9a94dacc505614a6516a95f"
|
||||
@@ -18170,6 +18165,11 @@ type-fest@^0.8.1:
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-fest@^2.19.0:
|
||||
version "2.19.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b"
|
||||
integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==
|
||||
|
||||
type-fest@^3.6.0:
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.6.0.tgz#827c36c0e7fcff0cb2d55d091a5c4cf586432b8a"
|
||||
@@ -19371,18 +19371,15 @@ yocto-queue@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||
|
||||
yup@^0.32.11:
|
||||
version "0.32.11"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.32.11.tgz#d67fb83eefa4698607982e63f7ca4c5ed3cf18c5"
|
||||
integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg==
|
||||
yup@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-1.2.0.tgz#9e51af0c63bdfc9be0fdc6c10aa0710899d8aff6"
|
||||
integrity sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.15.4"
|
||||
"@types/lodash" "^4.14.175"
|
||||
lodash "^4.17.21"
|
||||
lodash-es "^4.17.21"
|
||||
nanoclone "^0.2.1"
|
||||
property-expr "^2.0.4"
|
||||
property-expr "^2.0.5"
|
||||
tiny-case "^1.0.3"
|
||||
toposort "^2.0.2"
|
||||
type-fest "^2.19.0"
|
||||
|
||||
zwitch@^1.0.0:
|
||||
version "1.0.5"
|
||||
|
||||
Reference in New Issue
Block a user