Merge branch 'main' of github.com:Chroma-Case/Chromacase into feature/adc/artist-view

This commit is contained in:
Arthur Jamet
2023-07-05 14:06:27 +01:00
21 changed files with 579 additions and 291 deletions

View File

@@ -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,22 +269,13 @@ 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 }
),
};
}
@@ -292,10 +298,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,
}
),
};
}
@@ -304,7 +314,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`;
}
/**
@@ -312,7 +322,7 @@ 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`;
}
/**
@@ -323,10 +333,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 }
),
};
}
@@ -337,9 +349,12 @@ export default class API {
return {
key: ['artist', artistId],
exec: () =>
API.fetch({
route: `/artist/${artistId}`,
}),
API.fetch(
{
route: `/artist/${artistId}`,
},
{ handler: ArtistHandler }
),
};
}
@@ -368,13 +383,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 }
),
};
}
@@ -386,9 +404,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) }
),
};
}
@@ -400,9 +421,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) }
),
};
}
@@ -410,31 +434,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',
},
],
};
}
@@ -445,9 +465,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) }
),
};
}
@@ -459,7 +482,7 @@ export default class API {
return {
key: ['lesson', lessonId],
exec: async () => ({
title: 'Song',
name: 'Song',
description: 'A song',
requiredLevel: 1,
mainSkill: 'lead-head-change',
@@ -478,22 +501,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) }
),
};
}
@@ -528,13 +541,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) }
),
};
}
@@ -555,36 +571,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 }
);
}
}

View 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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
View 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)),
});

View File

@@ -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
View 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)),
}),
});

View 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,8 +0,0 @@
interface UserData {
gamesPlayed: number;
xp: number;
avatar: string | undefined;
createdAt: Date;
}
export default UserData;

View File

@@ -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;

View File

@@ -68,7 +68,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",
@@ -87,11 +87,11 @@
"@types/react": "~18.0.24",
"@types/react-native": "~0.70.6",
"@types/react-navigation": "^3.4.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.0.0",
"babel-loader": "^8.3.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"chromatic": "^6.14.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",

View File

@@ -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 {

View File

@@ -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==
@@ -12591,11 +12591,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"
@@ -13705,11 +13700,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"
@@ -15423,7 +15413,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==
@@ -17931,6 +17921,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"
@@ -18165,6 +18160,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"
@@ -19366,18 +19366,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"