Front: Pretty and Lint (#225)

This commit is contained in:
Arthur Jamet
2023-06-17 07:01:23 +01:00
committed by GitHub
parent 399c7d0d9e
commit c5d465df97
94 changed files with 3627 additions and 3089 deletions

View File

@@ -45,6 +45,10 @@ jobs:
- name: Type Check - name: Type Check
run: yarn tsc run: yarn tsc
- name: Check Prettier
run: yarn pretty:check .
- name: Run Linter
run: yarn lint
- name: 🏗 Setup Expo - name: 🏗 Setup Expo
uses: expo/expo-github-action@v7 uses: expo/expo-github-action@v7

24
front/.eslintrc.json Normal file
View File

@@ -0,0 +1,24 @@
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": ["./tsconfig.json"] },
"plugins": ["react", "@typescript-eslint"],
"ignorePatterns": [
"node_modules/",
"webpack.config.js",
"babel.config.js",
"*.test.*",
"app.config.ts"
],
"rules": {
"react/react-in-jsx-scope": "off",
"@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"
}
}

5
front/.prettierignore Normal file
View File

@@ -0,0 +1,5 @@
.expo
.expo-shared/
dist/
.vscode/
.storybook/

13
front/.prettierrc.json Normal file
View File

@@ -0,0 +1,13 @@
{
"printWidth": 100,
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always"
}

View File

@@ -1,21 +1,21 @@
import Artist from "./models/Artist"; import Artist from './models/Artist';
import Album from "./models/Album"; import Album from './models/Album';
import AuthToken from "./models/AuthToken"; import AuthToken from './models/AuthToken';
import Chapter from "./models/Chapter"; import Chapter from './models/Chapter';
import Lesson from "./models/Lesson"; import Lesson from './models/Lesson';
import Genre from "./models/Genre"; import Genre from './models/Genre';
import LessonHistory from "./models/LessonHistory"; import LessonHistory from './models/LessonHistory';
import Song from "./models/Song"; import Song from './models/Song';
import SongHistory from "./models/SongHistory"; import SongHistory from './models/SongHistory';
import User from "./models/User"; import User from './models/User';
import Constants from "expo-constants"; import Constants from 'expo-constants';
import store from "./state/Store"; import store from './state/Store';
import { Platform } from "react-native"; import { Platform } from 'react-native';
import { en } from "./i18n/Translations"; import { en } from './i18n/Translations';
import { useQuery, QueryClient } from "react-query"; import { QueryClient } from 'react-query';
import UserSettings from "./models/UserSettings"; import UserSettings from './models/UserSettings';
import { PartialDeep } from "type-fest"; import { PartialDeep } from 'type-fest';
import SearchHistory from "./models/SearchHistory"; import SearchHistory from './models/SearchHistory';
type AuthenticationInput = { username: string; password: string }; type AuthenticationInput = { username: string; password: string };
type RegistrationInput = AuthenticationInput & { email: string }; type RegistrationInput = AuthenticationInput & { email: string };
@@ -24,8 +24,8 @@ export type AccessToken = string;
type FetchParams = { type FetchParams = {
route: string; route: string;
body?: Object; body?: object;
method?: "GET" | "POST" | "DELETE" | "PATCH" | "PUT"; method?: 'GET' | 'POST' | 'DELETE' | 'PATCH' | 'PUT';
// If true, No JSON parsing is done, the raw response's content is returned // If true, No JSON parsing is done, the raw response's content is returned
raw?: true; raw?: true;
}; };
@@ -40,7 +40,7 @@ export class APIError extends Error {
public status: number, public status: number,
// Set the message to the correct error this is a placeholder // Set the message to the correct error this is a placeholder
// when the error is only used internally (middleman) // when the error is only used internally (middleman)
public userMessage: keyof typeof en = "unknownError" public userMessage: keyof typeof en = 'unknownError'
) { ) {
super(message); super(message);
} }
@@ -48,24 +48,22 @@ export class APIError extends Error {
// we will need the same thing for the scorometer API url // we will need the same thing for the scorometer API url
const baseAPIUrl = const baseAPIUrl =
process.env.NODE_ENV != "development" && Platform.OS === "web" process.env.NODE_ENV != 'development' && Platform.OS === 'web'
? "/api" ? '/api'
: Constants.manifest?.extra?.apiUrl; : Constants.manifest?.extra?.apiUrl;
export default class API { export default class API {
public static async fetch(params: FetchParams) { public static async fetch(params: FetchParams) {
const jwtToken = store.getState().user.accessToken; const jwtToken = store.getState().user.accessToken;
const header = { const header = {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}; };
const response = await fetch(`${baseAPIUrl}${params.route}`, { const response = await fetch(`${baseAPIUrl}${params.route}`, {
headers: headers: (jwtToken && { ...header, Authorization: `Bearer ${jwtToken}` }) || header,
(jwtToken && { ...header, Authorization: `Bearer ${jwtToken}` }) ||
header,
body: JSON.stringify(params.body), body: JSON.stringify(params.body),
method: params.method ?? "GET", method: params.method ?? 'GET',
}).catch(() => { }).catch(() => {
throw new Error("Error while fetching API: " + baseAPIUrl); throw new Error('Error while fetching API: ' + baseAPIUrl);
}); });
if (params.raw) { if (params.raw) {
return response.arrayBuffer(); return response.arrayBuffer();
@@ -74,15 +72,11 @@ export default class API {
try { try {
const jsonResponse = body.length != 0 ? JSON.parse(body) : {}; const jsonResponse = body.length != 0 ? JSON.parse(body) : {};
if (!response.ok) { if (!response.ok) {
throw new APIError( throw new APIError(jsonResponse ?? response.statusText, response.status);
jsonResponse ?? response.statusText,
response.status
);
} }
return jsonResponse; return jsonResponse;
} catch (e) { } catch (e) {
if (e instanceof SyntaxError) if (e instanceof SyntaxError) throw new Error("Error while parsing Server's response");
throw new Error("Error while parsing Server's response");
throw e; throw e;
} }
} }
@@ -91,16 +85,16 @@ export default class API {
authenticationInput: AuthenticationInput authenticationInput: AuthenticationInput
): Promise<AccessToken> { ): Promise<AccessToken> {
return API.fetch({ return API.fetch({
route: "/auth/login", route: '/auth/login',
body: authenticationInput, body: authenticationInput,
method: "POST", method: 'POST',
}) })
.then((responseBody) => responseBody.access_token) .then((responseBody) => responseBody.access_token)
.catch((e) => { .catch((e) => {
if (!(e instanceof APIError)) throw e; if (!(e instanceof APIError)) throw e;
if (e.status == 401) if (e.status == 401)
throw new APIError("invalidCredentials", 401, "invalidCredentials"); throw new APIError('invalidCredentials', 401, 'invalidCredentials');
throw e; throw e;
}); });
} }
@@ -109,13 +103,11 @@ export default class API {
* @param registrationInput the credentials to create a new profile * @param registrationInput the credentials to create a new profile
* @returns A Promise. On success, will be resolved into an instance of the API wrapper * @returns A Promise. On success, will be resolved into an instance of the API wrapper
*/ */
public static async createAccount( public static async createAccount(registrationInput: RegistrationInput): Promise<AccessToken> {
registrationInput: RegistrationInput
): Promise<AccessToken> {
await API.fetch({ await API.fetch({
route: "/auth/register", route: '/auth/register',
body: registrationInput, body: registrationInput,
method: "POST", method: 'POST',
}); });
// In the Future we should move autheticate out of this function // In the Future we should move autheticate out of this function
// and maybe create a new function to create and login in one go // and maybe create a new function to create and login in one go
@@ -126,22 +118,19 @@ export default class API {
} }
public static async createAndGetGuestAccount(): Promise<AccessToken> { public static async createAndGetGuestAccount(): Promise<AccessToken> {
let response = await API.fetch({ const response = await API.fetch({
route: "/auth/guest", route: '/auth/guest',
method: "POST", method: 'POST',
}); });
if (!response.access_token) if (!response.access_token) throw new APIError('No access token', response.status);
throw new APIError("No access token", response.status);
return response.access_token; return response.access_token;
} }
public static async transformGuestToUser( public static async transformGuestToUser(registrationInput: RegistrationInput): Promise<void> {
registrationInput: RegistrationInput
): Promise<void> {
await API.fetch({ await API.fetch({
route: "/auth/me", route: '/auth/me',
body: registrationInput, body: registrationInput,
method: "PUT", method: 'PUT',
}); });
} }
@@ -149,8 +138,8 @@ export default class API {
* Retrieve information of the currently authentified user * Retrieve information of the currently authentified user
*/ */
public static async getUserInfo(): Promise<User> { public static async getUserInfo(): Promise<User> {
let user = await API.fetch({ const user = await API.fetch({
route: "/auth/me", route: '/auth/me',
}); });
// this a dummy settings object, we will need to fetch the real one from the API // this a dummy settings object, we will need to fetch the real one from the API
@@ -163,16 +152,15 @@ export default class API {
data: { data: {
gamesPlayed: user.partyPlayed as number, gamesPlayed: user.partyPlayed as number,
xp: 0, xp: 0,
createdAt: new Date("2023-04-09T00:00:00.000Z"), createdAt: new Date('2023-04-09T00:00:00.000Z'),
avatar: avatar: 'https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn',
"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> { public static async getUserSettings(): Promise<UserSettings> {
const settings = await API.fetch({ const settings = await API.fetch({
route: "/auth/me/settings", route: '/auth/me/settings',
}); });
return { return {
@@ -189,9 +177,7 @@ export default class API {
}; };
} }
public static async updateUserSettings( public static async updateUserSettings(settings: PartialDeep<UserSettings>): Promise<void> {
settings: PartialDeep<UserSettings>
): Promise<void> {
const dto = { const dto = {
pushNotification: settings.notifications?.pushNotif, pushNotification: settings.notifications?.pushNotif,
emailNotification: settings.notifications?.emailNotif, emailNotification: settings.notifications?.emailNotif,
@@ -203,8 +189,8 @@ export default class API {
showActivity: settings.showActivity, showActivity: settings.showActivity,
}; };
return API.fetch({ return API.fetch({
method: "PATCH", method: 'PATCH',
route: "/auth/me/settings", route: '/auth/me/settings',
body: dto, body: dto,
}); });
} }
@@ -225,27 +211,29 @@ export default class API {
*/ */
public static async authWithGoogle(): Promise<AuthToken> { public static async authWithGoogle(): Promise<AuthToken> {
//TODO //TODO
return "11111"; return '11111';
} }
public static async getAllSongs(): Promise<Song[]> { public static async getAllSongs(): Promise<Song[]> {
let songs = await API.fetch({ const songs = await API.fetch({
route: "/song", route: '/song',
}); });
// this is a dummy illustration, we will need to fetch the real one from the API // this is a dummy illustration, we will need to fetch the real one from the API
return songs.data.map( return songs.data.map(
// To be fixed with #168
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(song: any) => (song: any) =>
({ ({
id: song.id as number, id: song.id as number,
name: song.name as string, name: song.name as string,
artistId: song.artistId as number, artistId: song.artistId as number,
albumId: song.albumId as number, albumId: song.albumId as number,
genreId: song.genreId as number, genreId: song.genreId as number,
details: song.difficulties, details: song.difficulties,
cover: `${baseAPIUrl}/song/${song.id}/illustration`, cover: `${baseAPIUrl}/song/${song.id}/illustration`,
metrics: {}, metrics: {},
} as Song) } as Song)
); );
} }
@@ -254,7 +242,7 @@ export default class API {
* @param songId the id to find the song * @param songId the id to find the song
*/ */
public static async getSong(songId: number): Promise<Song> { public static async getSong(songId: number): Promise<Song> {
let song = await API.fetch({ const song = await API.fetch({
route: `/song/${songId}`, route: `/song/${songId}`,
}); });
@@ -326,8 +314,8 @@ export default class API {
end: 100 * value, end: 100 * value,
songId: songId, songId: songId,
name: `Chapter ${value}`, name: `Chapter ${value}`,
type: "chorus", type: 'chorus',
key_aspect: "rhythm", key_aspect: 'rhythm',
difficulty: value, difficulty: value,
id: value * 10, id: value * 10,
})); }));
@@ -369,23 +357,26 @@ export default class API {
* Search Album by name * Search Album by name
* @param query the string used to find the album * @param query the string used to find the album
*/ */
public static async searchAlbum(query?: string): Promise<Album[]> { public static async searchAlbum(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
query?: string
): Promise<Album[]> {
return [ return [
{ {
id: 1, id: 1,
name: "Super Trooper", name: 'Super Trooper',
}, },
{ {
id: 2, id: 2,
name: "Kingdom Heart 365/2 OST", name: 'Kingdom Heart 365/2 OST',
}, },
{ {
id: 3, id: 3,
name: "The Legend Of Zelda Ocarina Of Time OST", name: 'The Legend Of Zelda Ocarina Of Time OST',
}, },
{ {
id: 4, id: 4,
name: "Random Access Memories", name: 'Random Access Memories',
}, },
] as Album[]; ] as Album[];
} }
@@ -405,10 +396,10 @@ export default class API {
*/ */
public static async getLesson(lessonId: number): Promise<Lesson> { public static async getLesson(lessonId: number): Promise<Lesson> {
return { return {
title: "Song", title: 'Song',
description: "A song", description: 'A song',
requiredLevel: 1, requiredLevel: 1,
mainSkill: "lead-head-change", mainSkill: 'lead-head-change',
id: lessonId, id: lessonId,
}; };
} }
@@ -419,24 +410,26 @@ export default class API {
* @param take how much do we take to return * @param take how much do we take to return
* @returns Returns an array of history entries (temporary type any) * @returns Returns an array of history entries (temporary type any)
*/ */
public static async getSearchHistory( public static async getSearchHistory(skip?: number, take?: number): Promise<SearchHistory[]> {
skip?: number,
take?: number
): Promise<SearchHistory[]> {
return ( return (
await API.fetch({ (
route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`, await API.fetch({
method: "GET", route: `/history/search?skip=${skip ?? 0}&take=${take ?? 5}`,
}) method: 'GET',
).map((e: any) => { })
return { )
id: e.id, // To be fixed with #168
query: e.query, // eslint-disable-next-line @typescript-eslint/no-explicit-any
type: e.type, .map((e: any) => {
userId: e.userId, return {
timestamp: new Date(e.searchDate), id: e.id,
} as SearchHistory; query: e.query,
}); type: e.type,
userId: e.userId,
timestamp: new Date(e.searchDate),
} as SearchHistory;
})
);
} }
/** /**
@@ -446,14 +439,10 @@ export default class API {
* @param timestamp the date it's been issued * @param timestamp the date it's been issued
* @returns nothing * @returns nothing
*/ */
public static async createSearchHistoryEntry( public static async createSearchHistoryEntry(query: string, type: string): Promise<void> {
query: string,
type: string,
timestamp: number
): Promise<void> {
return await API.fetch({ return await API.fetch({
route: `/history/search`, route: `/history/search`,
method: "POST", method: 'POST',
body: { body: {
query: query, query: query,
type: type, type: type,
@@ -467,7 +456,7 @@ export default class API {
*/ */
public static async getSongSuggestions(): Promise<Song[]> { public static async getSongSuggestions(): Promise<Song[]> {
const queryClient = new QueryClient(); const queryClient = new QueryClient();
return await queryClient.fetchQuery(["API", "allsongs"], API.getAllSongs); return await queryClient.fetchQuery(['API', 'allsongs'], API.getAllSongs);
} }
/** /**
@@ -476,7 +465,7 @@ export default class API {
*/ */
public static async getUserPlayHistory(): Promise<SongHistory[]> { public static async getUserPlayHistory(): Promise<SongHistory[]> {
return this.fetch({ return this.fetch({
route: "/history", route: '/history',
}); });
} }
@@ -484,9 +473,7 @@ export default class API {
* Retrieve a lesson's history * Retrieve a lesson's history
* @param lessonId the id to find the lesson * @param lessonId the id to find the lesson
*/ */
public static async getLessonHistory( public static async getLessonHistory(lessonId: number): Promise<LessonHistory[]> {
lessonId: number
): Promise<LessonHistory[]> {
return [ return [
{ {
lessonId, lessonId,
@@ -497,50 +484,51 @@ export default class API {
/** /**
* Retrieve a partition images * Retrieve a partition images
* @param songId the id of the song * @param _songId the id of the song
* This API may be merged with the fetch song in the future * This API may be merged with the fetch song in the future
*/ */
public static async getPartitionRessources( public static async getPartitionRessources(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
songId: number songId: number
): Promise<[string, number, number][]> { ): Promise<[string, number, number][]> {
return [ return [
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469560426545222/vivaldi_split_1.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469560426545222/vivaldi_split_1.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469560900505660/vivaldi_split_2.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469560900505660/vivaldi_split_2.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469561261203506/vivaldi_split_3.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469561261203506/vivaldi_split_3.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469561546424381/vivaldi_split_4.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469561546424381/vivaldi_split_4.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469562058133564/vivaldi_split_5.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469562058133564/vivaldi_split_5.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469562347528202/vivaldi_split_6.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469562347528202/vivaldi_split_6.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469562792136815/vivaldi_split_7.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469562792136815/vivaldi_split_7.png',
1868, 1868,
400, 400,
], ],
[ [
"https://media.discordapp.net/attachments/717080637038788731/1067469563073142804/vivaldi_split_8.png", 'https://media.discordapp.net/attachments/717080637038788731/1067469563073142804/vivaldi_split_8.png',
1868, 1868,
400, 400,
], ],
@@ -549,8 +537,8 @@ export default class API {
public static async updateUserEmail(newEmail: string): Promise<User> { public static async updateUserEmail(newEmail: string): Promise<User> {
const rep = await API.fetch({ const rep = await API.fetch({
route: "/auth/me", route: '/auth/me',
method: "PUT", method: 'PUT',
body: { body: {
email: newEmail, email: newEmail,
}, },
@@ -567,8 +555,8 @@ export default class API {
newPassword: string newPassword: string
): Promise<User> { ): Promise<User> {
const rep = await API.fetch({ const rep = await API.fetch({
route: "/auth/me", route: '/auth/me',
method: "PUT", method: 'PUT',
body: { body: {
oldPassword: oldPassword, oldPassword: oldPassword,
password: newPassword, password: newPassword,

View File

@@ -5,21 +5,20 @@ import store, { persistor } from './state/Store';
import { Router } from './Navigation'; import { Router } from './Navigation';
import './i18n/i18n'; import './i18n/i18n';
import * as SplashScreen from 'expo-splash-screen'; import * as SplashScreen from 'expo-splash-screen';
import { PersistGate } from "redux-persist/integration/react"; import { PersistGate } from 'redux-persist/integration/react';
import LanguageGate from "./i18n/LanguageGate"; import LanguageGate from './i18n/LanguageGate';
import ThemeProvider, { ColorSchemeProvider } from './Theme'; import ThemeProvider, { ColorSchemeProvider } from './Theme';
import 'react-native-url-polyfill/auto'; import 'react-native-url-polyfill/auto';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}, },
}, },
}); });
export default function App() { export default function App() {
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
setTimeout(SplashScreen.hideAsync, 500); setTimeout(SplashScreen.hideAsync, 500);
@@ -30,7 +29,7 @@ export default function App() {
<ThemeProvider> <ThemeProvider>
<ColorSchemeProvider> <ColorSchemeProvider>
<LanguageGate> <LanguageGate>
<Router/> <Router />
</LanguageGate> </LanguageGate>
</ColorSchemeProvider> </ColorSchemeProvider>
</ThemeProvider> </ThemeProvider>

View File

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

View File

@@ -1,81 +1,84 @@
import { NativeBaseProvider, extendTheme, useColorMode, useTheme } from 'native-base'; import { NativeBaseProvider, extendTheme, useColorMode } from 'native-base';
import useColorScheme from './hooks/colorScheme'; import useColorScheme from './hooks/colorScheme';
import { useEffect } from 'react'; import { useEffect } from 'react';
const ThemeProvider = ({ children }: { children: JSX.Element }) => { const ThemeProvider = ({ children }: { children: JSX.Element }) => {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
return <NativeBaseProvider theme={extendTheme({ return (
config: { <NativeBaseProvider
useSystemColorMode: false, theme={extendTheme({
initialColorMode: colorScheme config: {
}, useSystemColorMode: false,
colors: { initialColorMode: colorScheme,
primary: { },
50: '#e6faea', colors: {
100: '#c8e7d0', primary: {
200: '#a7d6b5', 50: '#e6faea',
300: '#86c498', 100: '#c8e7d0',
400: '#65b47c', 200: '#a7d6b5',
500: '#4b9a62', 300: '#86c498',
600: '#3a784b', 400: '#65b47c',
700: '#275635', 500: '#4b9a62',
800: '#14341f', 600: '#3a784b',
900: '#001405', 700: '#275635',
}, 800: '#14341f',
secondary: { 900: '#001405',
50: '#d8ffff', },
100: '#acffff', secondary: {
200: '#7dffff', 50: '#d8ffff',
300: '#4dffff', 100: '#acffff',
400: '#28ffff', 200: '#7dffff',
500: '#18e5e6', 300: '#4dffff',
600: '#00b2b3', 400: '#28ffff',
700: '#007f80', 500: '#18e5e6',
800: '#004d4e', 600: '#00b2b3',
900: '#001b1d', 700: '#007f80',
}, 800: '#004d4e',
error: { 900: '#001b1d',
50: '#ffe2e9', },
100: '#ffb1bf', error: {
200: '#ff7f97', 50: '#ffe2e9',
300: '#ff4d6d', 100: '#ffb1bf',
400: '#fe1d43', 200: '#ff7f97',
500: '#e5062b', 300: '#ff4d6d',
600: '#b30020', 400: '#fe1d43',
700: '#810017', 500: '#e5062b',
800: '#4f000c', 600: '#b30020',
900: '#200004', 700: '#810017',
}, 800: '#4f000c',
notification: { 900: '#200004',
50: '#ffe1e1', },
100: '#ffb1b1', notification: {
200: '#ff7f7f', 50: '#ffe1e1',
300: '#ff4c4c', 100: '#ffb1b1',
400: '#ff1a1a', 200: '#ff7f7f',
500: '#e60000', 300: '#ff4c4c',
600: '#b40000', 400: '#ff1a1a',
700: '#810000', 500: '#e60000',
800: '#500000', 600: '#b40000',
900: '#210000', 700: '#810000',
} 800: '#500000',
}, 900: '#210000',
components: { },
Button: { },
variants: { components: {
solid: () => ({ Button: {
rounded: 'full', variants: {
}) solid: () => ({
} rounded: 'full',
} }),
} },
})}> },
{ children } },
</NativeBaseProvider>; })}
} >
{children}
const ColorSchemeProvider = (props: { children: any }) => { </NativeBaseProvider>
);
};
const ColorSchemeProvider = (props: { children: JSX.Element }) => {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const colorMode = useColorMode(); const colorMode = useColorMode();
@@ -83,7 +86,7 @@ const ColorSchemeProvider = (props: { children: any }) => {
colorMode.setColorMode(colorScheme); colorMode.setColorMode(colorScheme);
}, [colorScheme]); }, [colorScheme]);
return props.children; return props.children;
} };
export default ThemeProvider; export default ThemeProvider;
export { ColorSchemeProvider }; export { ColorSchemeProvider };

View File

@@ -1,41 +1,39 @@
module.exports = { module.exports = {
"name": "Chromacase", name: 'Chromacase',
"slug": "Chromacase", slug: 'Chromacase',
"version": "1.0.0", version: '1.0.0',
"orientation": "portrait", orientation: 'portrait',
"icon": "./assets/icon.png", icon: './assets/icon.png',
"userInterfaceStyle": "light", userInterfaceStyle: 'light',
"splash": { splash: {
"image": "./assets/splashLogo.png", image: './assets/splashLogo.png',
"resizeMode": "contain", resizeMode: 'contain',
"backgroundColor": "#ffffff" backgroundColor: '#ffffff',
}, },
"updates": { updates: {
"fallbackToCacheTimeout": 0 fallbackToCacheTimeout: 0,
}, },
"assetBundlePatterns": [ assetBundlePatterns: ['**/*'],
"**/*" ios: {
], supportsTablet: true,
"ios": { },
"supportsTablet": true android: {
}, adaptiveIcon: {
"android": { foregroundImage: './assets/adaptive-icon.png',
"adaptiveIcon": { backgroundColor: '#FFFFFF',
"foregroundImage": "./assets/adaptive-icon.png", package: 'com.chromacase.chromacase',
"backgroundColor": "#FFFFFF", versionCode: 1,
"package": "com.chromacase.chromacase", },
"versionCode": 1 package: 'build.apk',
}, },
"package": "build.apk" web: {
}, favicon: './assets/favicon.png',
"web": { },
"favicon": "./assets/favicon.png" extra: {
}, apiUrl: process.env.API_URL,
"extra": { scoroUrl: process.env.SCORO_URL,
apiUrl: process.env.API_URL, eas: {
scoroUrl: process.env.SCORO_URL, projectId: 'dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2',
"eas": { },
"projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2" },
} };
}
}

View File

@@ -1,39 +1,37 @@
{ {
"expo": { "expo": {
"name": "Chromacase", "name": "Chromacase",
"slug": "Chromacase", "slug": "Chromacase",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"userInterfaceStyle": "light", "userInterfaceStyle": "light",
"splash": { "splash": {
"image": "./assets/splashLogo.png", "image": "./assets/splashLogo.png",
"resizeMode": "cover", "resizeMode": "cover",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"updates": { "updates": {
"fallbackToCacheTimeout": 0 "fallbackToCacheTimeout": 0
}, },
"assetBundlePatterns": [ "assetBundlePatterns": ["**/*"],
"**/*" "ios": {
], "supportsTablet": true
"ios": { },
"supportsTablet": true "android": {
}, "adaptiveIcon": {
"android": { "foregroundImage": "./assets/adaptive-icon.png",
"adaptiveIcon": { "backgroundColor": "#FFFFFF"
"foregroundImage": "./assets/adaptive-icon.png", },
"backgroundColor": "#FFFFFF" "package": "build.apk"
}, },
"package": "build.apk" "web": {
}, "favicon": "./assets/favicon.png"
"web": { },
"favicon": "./assets/favicon.png" "extra": {
}, "eas": {
"extra": { "projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2"
"eas": { }
"projectId": "dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2" }
} }
}
}
} }

View File

@@ -1,14 +1,11 @@
module.exports = function (api) { module.exports = function (api) {
api.cache(true); api.cache(true);
return { return {
presets: ["babel-preset-expo"], presets: ['babel-preset-expo'],
plugins: [ plugins: ['@babel/plugin-proposal-export-namespace-from', 'react-native-reanimated/plugin'],
"@babel/plugin-proposal-export-namespace-from",
"react-native-reanimated/plugin",
],
env: { env: {
production: { production: {
plugins: ["react-native-paper/babel"], plugins: ['react-native-paper/babel'],
}, },
}, },
}; };

View File

@@ -1,7 +1,6 @@
import React from "react"; import React from 'react';
import Card, { CardBorderRadius } from "./Card"; import Card, { CardBorderRadius } from './Card';
import { VStack, Text, Image } from "native-base"; import { VStack, Text, Image } from 'native-base';
import API from "../API";
type ArtistCardProps = { type ArtistCardProps = {
image: string; image: string;
@@ -11,7 +10,7 @@ type ArtistCardProps = {
}; };
const ArtistCard = (props: ArtistCardProps) => { const ArtistCard = (props: ArtistCardProps) => {
const { image, name, id } = props; const { image, name } = props;
return ( return (
<Card shadow={3} onPress={props.onPress}> <Card shadow={3} onPress={props.onPress}>
@@ -32,10 +31,10 @@ const ArtistCard = (props: ArtistCardProps) => {
}; };
ArtistCard.defaultProps = { ArtistCard.defaultProps = {
image: "https://picsum.photos/200", image: 'https://picsum.photos/200',
name: "Artist", name: 'Artist',
id: 0, id: 0,
onPress: () => { }, onPress: () => {},
}; };
export default ArtistCard; export default ArtistCard;

View File

@@ -1,9 +1,7 @@
import React from "react"; import React from 'react';
import { import {
Box, Box,
Center,
Heading, Heading,
View,
Image, Image,
Text, Text,
Pressable, Pressable,
@@ -11,9 +9,9 @@ import {
Icon, Icon,
Row, Row,
PresenceTransition, PresenceTransition,
} from "native-base"; } from 'native-base';
import { StyleProp, ViewStyle } from "react-native"; import { StyleProp, ViewStyle } from 'react-native';
import useColorScheme from "../hooks/colorScheme"; import useColorScheme from '../hooks/colorScheme';
type BigActionButtonProps = { type BigActionButtonProps = {
title: string; title: string;
@@ -21,6 +19,8 @@ type BigActionButtonProps = {
image: string; image: string;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
iconName?: string; iconName?: string;
// It is not possible to recover the type, the `Icon` parameter is `any` as well.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
iconProvider?: any; iconProvider?: any;
onPress: () => void; onPress: () => void;
}; };
@@ -34,27 +34,27 @@ const BigActionButton = ({
iconProvider, iconProvider,
onPress, onPress,
}: BigActionButtonProps) => { }: BigActionButtonProps) => {
const screenSize = useBreakpointValue({ base: "small", md: "big" }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const isDark = colorScheme === "dark"; const isDark = colorScheme === 'dark';
return ( return (
<Pressable onPress={onPress} style={style}> <Pressable onPress={onPress} style={style}>
{({ isHovered, isPressed }) => { {({ isHovered }) => {
return ( return (
<Box <Box
style={{ style={{
width: "100%", width: '100%',
height: "100%", height: '100%',
position: "relative", position: 'relative',
borderRadius: 10, borderRadius: 10,
overflow: "hidden", overflow: 'hidden',
}} }}
> >
<PresenceTransition <PresenceTransition
style={{ style={{
width: "100%", width: '100%',
height: "100%", height: '100%',
}} }}
visible={isHovered} visible={isHovered}
initial={{ initial={{
@@ -68,15 +68,15 @@ const BigActionButton = ({
source={{ uri: image }} source={{ uri: image }}
alt="image" alt="image"
style={{ style={{
width: "100%", width: '100%',
height: "100%", height: '100%',
resizeMode: "cover", resizeMode: 'cover',
}} }}
/> />
</PresenceTransition> </PresenceTransition>
<PresenceTransition <PresenceTransition
style={{ style={{
height: "100%", height: '100%',
}} }}
visible={isHovered} visible={isHovered}
initial={{ initial={{
@@ -90,24 +90,24 @@ const BigActionButton = ({
> >
<Box <Box
style={{ style={{
position: "absolute", position: 'absolute',
left: "0", left: '0',
width: "100%", width: '100%',
height: "100%", height: '100%',
backgroundColor: isDark ? "black" : "white", backgroundColor: isDark ? 'black' : 'white',
padding: "10px", padding: '10px',
}} }}
> >
<Row> <Row>
<Icon <Icon
as={iconProvider} as={iconProvider}
name={iconName} name={iconName}
size={screenSize === "small" ? "sm" : "md"} size={screenSize === 'small' ? 'sm' : 'md'}
color={isDark ? "white" : "black"} color={isDark ? 'white' : 'black'}
marginRight="10px" marginRight="10px"
/> />
<Heading <Heading
fontSize={screenSize === "small" ? "md" : "xl"} fontSize={screenSize === 'small' ? 'md' : 'xl'}
isTruncated isTruncated
> >
{title} {title}
@@ -126,7 +126,7 @@ const BigActionButton = ({
}} }}
> >
<Text <Text
fontSize={screenSize === "small" ? "sm" : "md"} fontSize={screenSize === 'small' ? 'sm' : 'md'}
isTruncated isTruncated
noOfLines={2} noOfLines={2}
> >

View File

@@ -9,31 +9,39 @@ export const CardBorderRadius = 10;
const cardBorder = (theme: ReturnType<typeof useTheme>) => ({ const cardBorder = (theme: ReturnType<typeof useTheme>) => ({
borderColor: theme.colors.text[100], borderColor: theme.colors.text[100],
borderRadius: CardBorderRadius, borderRadius: CardBorderRadius,
borderWidth: 1 borderWidth: 1,
}) });
type CardProps = Parameters<typeof Box>[0] & { type CardProps = Parameters<typeof Box>[0] & {
onPress: () => void onPress: () => void;
} };
const Card = (props: CardProps) => { const Card = (props: CardProps) => {
const theme = useTheme(); const theme = useTheme();
const colorScheme = useSelector((state: RootState) => state.settings.local.colorScheme); const colorScheme = useSelector((state: RootState) => state.settings.local.colorScheme);
const systemColorMode = useColorScheme(); const systemColorMode = useColorScheme();
return <Pressable onPress={props.onPress}> return (
{({ isHovered, isPressed }) => ( <Pressable onPress={props.onPress}>
<Box {...props} style={[props.style, cardBorder(theme)]} {({ isHovered, isPressed }) => (
bg={(colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark' <Box
? (isHovered || isPressed) ? 'gray.800' : undefined {...props}
: (isHovered || isPressed) ? 'coolGray.200' : undefined style={[props.style, cardBorder(theme)]}
} bg={
> (colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
{ props.children } ? isHovered || isPressed
</Box> ? 'gray.800'
)} : undefined
</Pressable> : isHovered || isPressed
? 'coolGray.200'
} : undefined
}
>
{props.children}
</Box>
)}
</Pressable>
);
};
export default Card; export default Card;

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { FlatGrid } from 'react-native-super-grid'; import { FlatGrid } from 'react-native-super-grid';
import { Heading, VStack } from 'native-base'; import { Heading, VStack } from 'native-base';
type CardGridCustomProps<T> = { type CardGridCustomProps<T> = {
content: T[]; content: T[];
heading?: JSX.Element; heading?: JSX.Element;
@@ -11,7 +10,7 @@ type CardGridCustomProps<T> = {
cardComponent: React.ComponentType<T>; cardComponent: React.ComponentType<T>;
}; };
const CardGridCustom = <T extends Record<string, any>>(props: CardGridCustomProps<T>) => { const CardGridCustom = <T extends Record<string, unknown>>(props: CardGridCustomProps<T>) => {
const { content, heading, maxItemsPerRow, style, cardComponent: CardComponent } = props; const { content, heading, maxItemsPerRow, style, cardComponent: CardComponent } = props;
return ( return (
@@ -28,4 +27,4 @@ const CardGridCustom = <T extends Record<string, any>>(props: CardGridCustomProp
); );
}; };
export default CardGridCustom; export default CardGridCustom;

View File

@@ -1,6 +1,6 @@
import { useNavigation } from "../Navigation"; import { useNavigation } from '../Navigation';
import { HStack, VStack, Text, Progress } from "native-base"; import { HStack, VStack, Text, Progress } from 'native-base';
import { translate } from "../i18n/i18n"; import { translate } from '../i18n/i18n';
import Card from './Card'; import Card from './Card';
type CompetenciesTableProps = { type CompetenciesTableProps = {
@@ -10,26 +10,32 @@ type CompetenciesTableProps = {
accuracyCompetency: number; accuracyCompetency: number;
arpegeCompetency: number; arpegeCompetency: number;
chordsCompetency: number; chordsCompetency: number;
} };
const CompetenciesTable = (props: CompetenciesTableProps) => { const CompetenciesTable = (props: CompetenciesTableProps) => {
const navigation = useNavigation(); const navigation = useNavigation();
return ( return (
<Card padding={5} onPress={() => navigation.navigate('User')} shadow={3}> <Card padding={5} onPress={() => navigation.navigate('User')} shadow={3}>
<HStack space={5} flex={1}> <HStack space={5} flex={1}>
<VStack space={5}> <VStack space={5}>
{ Object.keys(props).map((competencyName, i) => ( {Object.keys(props).map((competencyName, i) => (
<Text bold key={i}>{translate(competencyName as keyof CompetenciesTableProps)}</Text> <Text bold key={i}>
))} {translate(competencyName as keyof CompetenciesTableProps)}
</Text>
))}
</VStack> </VStack>
<VStack space={5} flex={1}> <VStack space={5} flex={1}>
{ Object.keys(props).map((competencyName, i) => ( {Object.keys(props).map((competencyName, i) => (
<Progress key={i} flex={1} value={props[competencyName as keyof CompetenciesTableProps]} /> <Progress
))} key={i}
flex={1}
value={props[competencyName as keyof CompetenciesTableProps]}
/>
))}
</VStack> </VStack>
</HStack> </HStack>
</Card> </Card>
) );
} };
export default CompetenciesTable export default CompetenciesTable;

View File

@@ -1,9 +1,8 @@
import React from "react"; import React from 'react';
import Card from "./Card"; import Card from './Card';
import { VStack, Text, Box, Icon, Image } from "native-base"; import { VStack, Text, Box, Image } from 'native-base';
import { useTheme } from "native-base"; import { useTheme } from 'native-base';
import { Ionicons } from "@expo/vector-icons";
import API from "../API";
type GenreCardProps = { type GenreCardProps = {
image: string; image: string;
name: string; name: string;
@@ -12,7 +11,7 @@ type GenreCardProps = {
}; };
const GenreCard = (props: GenreCardProps) => { const GenreCard = (props: GenreCardProps) => {
const { image, name, id } = props; const { image, name } = props;
const theme = useTheme(); const theme = useTheme();
return ( return (
@@ -45,9 +44,9 @@ const GenreCard = (props: GenreCardProps) => {
}; };
GenreCard.defaultProps = { GenreCard.defaultProps = {
icon: "https://picsum.photos/200", icon: 'https://picsum.photos/200',
name: "Genre", name: 'Genre',
onPress: () => { }, onPress: () => {},
}; };
export default GenreCard; export default GenreCard;

View File

@@ -1,17 +1,16 @@
import React from "react"; import React from 'react';
import { ElementProps } from "./ElementTypes"; import { ElementProps } from './ElementTypes';
import { RawElement } from "./RawElement"; import { RawElement } from './RawElement';
import { Pressable, IPressableProps } from "native-base"; import { Pressable, IPressableProps } from 'native-base';
import { ElementTextProps, ElementToggleProps } from './ElementTypes';
export const Element = <T extends ElementProps,>(props: T) => { export const Element = <T extends ElementProps>(props: T) => {
let actionFunction: IPressableProps['onPress'] = null; let actionFunction: IPressableProps['onPress'] = null;
switch (props.type) { switch (props.type) {
case "text": case 'text':
actionFunction = props.data?.onPress; actionFunction = props.data?.onPress;
break; break;
case "toggle": case 'toggle':
actionFunction = props.data?.onToggle; actionFunction = props.data?.onToggle;
break; break;
default: default:

View File

@@ -1,14 +1,10 @@
import React from "react"; import React from 'react';
import { StyleProp, ViewStyle } from "react-native"; import { StyleProp, ViewStyle } from 'react-native';
import { Element } from "./Element"; import { Element } from './Element';
import useColorScheme from "../../hooks/colorScheme"; import useColorScheme from '../../hooks/colorScheme';
import { ElementProps } from "./ElementTypes"; import { ElementProps } from './ElementTypes';
import { import { Box, Column, Divider } from 'native-base';
Box,
Column,
Divider,
} from "native-base";
type ElementListProps = { type ElementListProps = {
elements: ElementProps[]; elements: ElementProps[];
@@ -17,23 +13,21 @@ type ElementListProps = {
const ElementList = ({ elements, style }: ElementListProps) => { const ElementList = ({ elements, style }: ElementListProps) => {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const isDark = colorScheme === "dark"; const isDark = colorScheme === 'dark';
const elementStyle = { const elementStyle = {
borderRadius: 10, borderRadius: 10,
boxShadow: isDark boxShadow: isDark
? "0px 0px 3px 0px rgba(255,255,255,0.6)" ? '0px 0px 3px 0px rgba(255,255,255,0.6)'
: "0px 0px 3px 0px rgba(0,0,0,0.4)", : '0px 0px 3px 0px rgba(0,0,0,0.4)',
overflow: "hidden", overflow: 'hidden',
} as const; } as const;
return ( return (
<Column style={[style, elementStyle]}> <Column style={[style, elementStyle]}>
{elements.map((element, index, __) => ( {elements.map((element, index) => (
<Box key={element.title}> <Box key={element.title}>
<Element {...element} /> <Element {...element} />
{ index < elements.length - 1 && {index < elements.length - 1 && <Divider />}
<Divider />
}
</Box> </Box>
))} ))}
</Column> </Column>

View File

@@ -1,5 +1,5 @@
import { Select, Switch, Text, Icon, Row, Slider } from "native-base"; import { Select, Switch, Text, Icon, Row, Slider } from 'native-base';
import { MaterialIcons } from "@expo/vector-icons"; import { MaterialIcons } from '@expo/vector-icons';
export type ElementProps = { export type ElementProps = {
title: string; title: string;
@@ -8,11 +8,11 @@ export type ElementProps = {
description?: string; description?: string;
disabled?: boolean; disabled?: boolean;
} & ( } & (
{ type: 'text', data : ElementTextProps } | | { type: 'text'; data: ElementTextProps }
{ type: 'toggle', data : ElementToggleProps } | | { type: 'toggle'; data: ElementToggleProps }
{ type: 'dropdown', data : ElementDropdownProps } | | { type: 'dropdown'; data: ElementDropdownProps }
{ type: 'range', data : ElementRangeProps } | | { type: 'range'; data: ElementRangeProps }
{ type: 'custom', data : React.ReactNode } | { type: 'custom'; data: React.ReactNode }
); );
export type DropdownOption = { export type DropdownOption = {
@@ -47,14 +47,11 @@ export type ElementRangeProps = {
step?: number; step?: number;
}; };
export const getElementTextNode = ( export const getElementTextNode = ({ text, onPress }: ElementTextProps, disabled: boolean) => {
{ text, onPress }: ElementTextProps,
disabled: boolean
) => {
return ( return (
<Row <Row
style={{ style={{
alignItems: "center", alignItems: 'center',
}} }}
> >
<Text <Text
@@ -79,7 +76,7 @@ export const getElementTextNode = (
}; };
export const getElementToggleNode = ( export const getElementToggleNode = (
{ onToggle, value, defaultValue }: ElementToggleProps, { value, defaultValue }: ElementToggleProps,
disabled: boolean disabled: boolean
) => { ) => {
return ( return (
@@ -105,18 +102,14 @@ export const getElementDropdownNode = (
isDisabled={disabled} isDisabled={disabled}
> >
{options.map((option) => ( {options.map((option) => (
<Select.Item <Select.Item key={option.label} label={option.label} value={option.value} />
key={option.label}
label={option.label}
value={option.value}
/>
))} ))}
</Select> </Select>
); );
}; };
export const getElementRangeNode = ( export const getElementRangeNode = (
{ onChange, value, defaultValue, min, max, step }: ElementRangeProps, { onChange, value, min, max, step }: ElementRangeProps,
disabled: boolean, disabled: boolean,
title: string title: string
) => { ) => {

View File

@@ -1,28 +1,14 @@
import React from "react"; import React from 'react';
import { import { Box, Button, Column, Icon, Popover, Row, Text, useBreakpointValue } from 'native-base';
Box, import useColorScheme from '../../hooks/colorScheme';
Button, import { Ionicons } from '@expo/vector-icons';
Column, import { ElementProps } from './ElementTypes';
Divider,
Icon,
Popover,
Row,
Text,
useBreakpointValue,
} from "native-base";
import useColorScheme from "../../hooks/colorScheme";
import { Ionicons } from "@expo/vector-icons";
import { ElementProps } from "./ElementTypes";
import { import {
getElementDropdownNode, getElementDropdownNode,
getElementTextNode, getElementTextNode,
getElementToggleNode, getElementToggleNode,
getElementRangeNode, getElementRangeNode,
ElementDropdownProps, } from './ElementTypes';
ElementTextProps,
ElementToggleProps,
ElementRangeProps,
} from "./ElementTypes";
type RawElementProps = { type RawElementProps = {
element: ElementProps; element: ElementProps;
@@ -30,25 +16,24 @@ type RawElementProps = {
}; };
export const RawElement = ({ element, isHovered }: RawElementProps) => { export const RawElement = ({ element, isHovered }: RawElementProps) => {
const { title, icon, type, helperText, description, disabled, data } = const { title, icon, type, helperText, description, disabled, data } = element;
element;
const colorScheme = useColorScheme(); const colorScheme = useColorScheme();
const isDark = colorScheme === "dark"; const isDark = colorScheme === 'dark';
const screenSize = useBreakpointValue({ base: "small", md: "big" }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isSmallScreen = screenSize === "small"; const isSmallScreen = screenSize === 'small';
return ( return (
<Row <Row
style={{ style={{
width: "100%", width: '100%',
height: 45, height: 45,
padding: 15, padding: 15,
justifyContent: "space-between", justifyContent: 'space-between',
alignContent: "stretch", alignContent: 'stretch',
alignItems: "center", alignItems: 'center',
backgroundColor: isHovered backgroundColor: isHovered
? isDark ? isDark
? "rgba(255, 255, 255, 0.1)" ? 'rgba(255, 255, 255, 0.1)'
: "rgba(0, 0, 0, 0.05)" : 'rgba(0, 0, 0, 0.05)'
: undefined, : undefined,
}} }}
> >
@@ -60,14 +45,14 @@ export const RawElement = ({ element, isHovered }: RawElementProps) => {
}} }}
> >
{icon} {icon}
<Column maxW={"90%"}> <Column maxW={'90%'}>
<Text isTruncated maxW={"100%"}> <Text isTruncated maxW={'100%'}>
{title} {title}
</Text> </Text>
{description && ( {description && (
<Text <Text
isTruncated isTruncated
maxW={"100%"} maxW={'100%'}
style={{ style={{
opacity: 0.6, opacity: 0.6,
fontSize: 12, fontSize: 12,
@@ -86,7 +71,7 @@ export const RawElement = ({ element, isHovered }: RawElementProps) => {
> >
<Row <Row
style={{ style={{
alignItems: "center", alignItems: 'center',
marginRight: 3, marginRight: 3,
}} }}
> >
@@ -99,7 +84,7 @@ export const RawElement = ({ element, isHovered }: RawElementProps) => {
leftIcon={ leftIcon={
<Icon <Icon
as={Ionicons} as={Ionicons}
size={"md"} size={'md'}
name="help-circle-outline" name="help-circle-outline"
/> />
} }
@@ -110,7 +95,7 @@ export const RawElement = ({ element, isHovered }: RawElementProps) => {
<Popover.Content <Popover.Content
accessibilityLabel={`Additionnal information for ${title}`} accessibilityLabel={`Additionnal information for ${title}`}
style={{ style={{
maxWidth: isSmallScreen ? "90vw" : "20vw", maxWidth: isSmallScreen ? '90vw' : '20vw',
}} }}
> >
<Popover.Arrow /> <Popover.Arrow />
@@ -120,15 +105,15 @@ export const RawElement = ({ element, isHovered }: RawElementProps) => {
)} )}
{(() => { {(() => {
switch (type) { switch (type) {
case "text": case 'text':
return getElementTextNode(data, disabled ?? false); return getElementTextNode(data, disabled ?? false);
case "toggle": case 'toggle':
return getElementToggleNode(data, disabled ?? false); return getElementToggleNode(data, disabled ?? false);
case "dropdown": case 'dropdown':
return getElementDropdownNode(data, disabled ?? false); return getElementDropdownNode(data, disabled ?? false);
case "range": case 'range':
return getElementRangeNode(data, disabled ?? false, title); return getElementRangeNode(data, disabled ?? false, title);
case "custom": case 'custom':
return data; return data;
default: default:
return <Text>Unknown type</Text>; return <Text>Unknown type</Text>;

View File

@@ -8,7 +8,9 @@ type SearchHistoryCardProps = {
timestamp?: string; timestamp?: string;
}; };
const SearchHistoryCard = (props: SearchHistoryCardProps & { onPress: (query: string) => void }) => { const SearchHistoryCard = (
props: SearchHistoryCardProps & { onPress: (query: string) => void }
) => {
const { query, type, timestamp, onPress } = props; const { query, type, timestamp, onPress } = props;
const handlePress = () => { const handlePress = () => {
@@ -18,18 +20,18 @@ const SearchHistoryCard = (props: SearchHistoryCardProps & { onPress: (query: st
}; };
return ( return (
<Card shadow={2} onPress={handlePress} > <Card shadow={2} onPress={handlePress}>
<VStack m={1.5} space={3}> <VStack m={1.5} space={3}>
<Text fontSize="lg" fontWeight="bold"> <Text fontSize="lg" fontWeight="bold">
{query ?? "query"} {query ?? 'query'}
</Text> </Text>
<Text fontSize="lg" fontWeight="semibold"> <Text fontSize="lg" fontWeight="semibold">
{type ?? "type"} {type ?? 'type'}
</Text> </Text>
<Text color="gray.500">{timestamp ?? "timestamp"}</Text> <Text color="gray.500">{timestamp ?? 'timestamp'}</Text>
</VStack> </VStack>
</Card> </Card>
); );
}; };
export default SearchHistoryCard; export default SearchHistoryCard;

View File

@@ -1,12 +1,16 @@
import { Box, Button } from "native-base"; import { Box, Button } from 'native-base';
type IconButtonProps = { type IconButtonProps = {
icon: Parameters<typeof Button>[0]['leftIcon'] icon: Parameters<typeof Button>[0]['leftIcon'];
} & Omit<Parameters<typeof Button>[0], 'leftIcon' | 'rightIcon'>; } & Omit<Parameters<typeof Button>[0], 'leftIcon' | 'rightIcon'>;
// Wrapper around Button for IconButton as Native's one sucks <3 // Wrapper around Button for IconButton as Native's one sucks <3
const IconButton = (props: IconButtonProps) => { const IconButton = (props: IconButtonProps) => {
return <Box><Button {...props} leftIcon={props.icon} width='fit-content' rounded='sm'/></Box> return (
} <Box>
<Button {...props} leftIcon={props.icon} width="fit-content" rounded="sm" />
</Box>
);
};
export default IconButton; export default IconButton;

View File

@@ -1,8 +1,8 @@
import { ComponentStory, ComponentMeta } from "@storybook/react"; import { ComponentStory, ComponentMeta } from '@storybook/react';
import Loading from "./Loading"; import Loading from './Loading';
export default { export default {
title: "Loading", title: 'Loading',
component: Loading, component: Loading,
} as ComponentMeta<typeof Loading>; } as ComponentMeta<typeof Loading>;

View File

@@ -1,15 +1,17 @@
import { useTheme } from "native-base"; import { useTheme } from 'native-base';
import { Center, Spinner } from "native-base"; import { Center, Spinner } from 'native-base';
const LoadingComponent = () => { const LoadingComponent = () => {
const theme = useTheme(); const theme = useTheme();
return <Spinner color={theme.colors.primary[500]}/> return <Spinner color={theme.colors.primary[500]} />;
} };
const LoadingView = () => { const LoadingView = () => {
return <Center style={{ flexGrow: 1 }}> return (
<LoadingComponent/> <Center style={{ flexGrow: 1 }}>
</Center> <LoadingComponent />
} </Center>
);
};
export default LoadingComponent; export default LoadingComponent;
export { LoadingView } export { LoadingView };

View File

@@ -1,7 +1,14 @@
/* eslint-disable no-mixed-spaces-and-tabs */
// Inspired from OSMD example project // Inspired from OSMD example project
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx // https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { CursorType, Fraction, OpenSheetMusicDisplay as OSMD, IOSMDOptions, Note, Pitch } from 'opensheetmusicdisplay'; import {
CursorType,
Fraction,
OpenSheetMusicDisplay as OSMD,
IOSMDOptions,
Note,
} from 'opensheetmusicdisplay';
import useColorScheme from '../hooks/colorScheme'; import useColorScheme from '../hooks/colorScheme';
import { useWindowDimensions } from 'react-native'; import { useWindowDimensions } from 'react-native';
import SoundFont from 'soundfont-player'; import SoundFont from 'soundfont-player';
@@ -12,9 +19,9 @@ type PartitionViewProps = {
file: string; file: string;
onPartitionReady: () => void; onPartitionReady: () => void;
onEndReached: () => void; onEndReached: () => void;
// Timestamp of the play session, in milisecond // Timestamp of the play session, in milisecond
timestamp: number; timestamp: number;
} };
const PartitionView = (props: PartitionViewProps) => { const PartitionView = (props: PartitionViewProps) => {
const [osmd, setOsmd] = useState<OSMD>(); const [osmd, setOsmd] = useState<OSMD>();
@@ -34,15 +41,15 @@ const PartitionView = (props: PartitionViewProps) => {
renderSingleHorizontalStaffline: true, renderSingleHorizontalStaffline: true,
cursorsOptions: [{ type: CursorType.Standard, color: 'green', alpha: 0.5, follow: false }], cursorsOptions: [{ type: CursorType.Standard, color: 'green', alpha: 0.5, follow: false }],
autoResize: false, autoResize: false,
} };
// Turns note.Length or timestamp in ms // Turns note.Length or timestamp in ms
const timestampToMs = (timestamp: Fraction) => { const timestampToMs = (timestamp: Fraction) => {
return timestamp.RealValue * wholeNoteLength; return timestamp.RealValue * wholeNoteLength;
} };
const getActualNoteLength = (note: Note) => { const getActualNoteLength = (note: Note) => {
let duration = timestampToMs(note.Length) let duration = timestampToMs(note.Length);
if (note.NoteTie) { if (note.NoteTie) {
const firstNote = note.NoteTie.Notes.at(1) const firstNote = note.NoteTie.Notes.at(1);
if (Object.is(note.NoteTie.StartNote, note) && firstNote) { if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
duration += timestampToMs(firstNote.Length); duration += timestampToMs(firstNote.Length);
} else { } else {
@@ -50,44 +57,52 @@ const PartitionView = (props: PartitionViewProps) => {
} }
} }
return duration; return duration;
} };
const playNotesUnderCursor = () => { const playNotesUnderCursor = () => {
osmd!.cursor.NotesUnderCursor() osmd!.cursor
.NotesUnderCursor()
.filter((note) => note.isRest() == false) .filter((note) => note.isRest() == false)
.filter((note) => note.Pitch) // Pitch Can be null, avoiding them .filter((note) => note.Pitch) // Pitch Can be null, avoiding them
.forEach((note) => { .forEach((note) => {
// Put your hands together for https://github.com/jimutt/osmd-audio-player/blob/master/src/internals/noteHelpers.ts // Put your hands together for https://github.com/jimutt/osmd-audio-player/blob/master/src/internals/noteHelpers.ts
const fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)?.fixedKey ?? 0; const fixedKey =
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)?.fixedKey ?? 0;
const midiNumber = note.halfTone - fixedKey * 12; const midiNumber = note.halfTone - fixedKey * 12;
// console.log('Expecting midi ' + midiNumber); // console.log('Expecting midi ' + midiNumber);
let duration = getActualNoteLength(note); const duration = getActualNoteLength(note);
const gain = note.ParentVoiceEntry.ParentVoice.Volume; const gain = note.ParentVoiceEntry.ParentVoice.Volume;
soundPlayer!.play(midiNumber.toString(), audioContext.currentTime, { duration, gain }) soundPlayer!.play(midiNumber.toString(), audioContext.currentTime, {
duration,
gain,
});
}); });
} };
const getShortedNoteUnderCursor = () => { const getShortedNoteUnderCursor = () => {
return osmd!.cursor.NotesUnderCursor().sort((n1, n2) => n1.Length.CompareTo(n2.Length)).at(0); return osmd!.cursor
} .NotesUnderCursor()
.sort((n1, n2) => n1.Length.CompareTo(n2.Length))
.at(0);
};
useEffect(() => { useEffect(() => {
const _osmd = new OSMD(OSMD_DIV_ID, options); const _osmd = new OSMD(OSMD_DIV_ID, options);
Promise.all([ Promise.all([
SoundFont.instrument(audioContext as unknown as AudioContext, 'electric_piano_1'), SoundFont.instrument(audioContext as unknown as AudioContext, 'electric_piano_1'),
_osmd.load(props.file) _osmd.load(props.file),
]).then(([player, __]) => { ]).then(([player]) => {
setSoundPlayer(player); setSoundPlayer(player);
_osmd.render(); _osmd.render();
_osmd.cursor.hide(); _osmd.cursor.hide();
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63 // Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60; const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
setWholeNoteLength(Math.round((60 / bpm) * 4000)) setWholeNoteLength(Math.round((60 / bpm) * 4000));
props.onPartitionReady(); props.onPartitionReady();
// Do not show cursor before actuall start // Do not show cursor before actuall start
}); });
setOsmd(_osmd); setOsmd(_osmd);
}, []); }, []);
// Re-render manually (otherwise done by 'autoResize' option), to fix disappearing cursor // Re-render manually (otherwise done by 'autoResize' option), to fix disappearing cursor
useEffect(() => { useEffect(() => {
if (osmd && osmd.IsReadyToRender()) { if (osmd && osmd.IsReadyToRender()) {
@@ -96,7 +111,7 @@ const PartitionView = (props: PartitionViewProps) => {
osmd.cursor.show(); osmd.cursor.show();
} }
} }
}, [dimensions]) }, [dimensions]);
useEffect(() => { useEffect(() => {
if (!osmd || !soundPlayer) { if (!osmd || !soundPlayer) {
@@ -110,10 +125,14 @@ const PartitionView = (props: PartitionViewProps) => {
let previousCursorPosition = -1; let previousCursorPosition = -1;
let currentCursorPosition = osmd.cursor.cursorElement.offsetLeft; let currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
let shortestNote = getShortedNoteUnderCursor(); let shortestNote = getShortedNoteUnderCursor();
while(!osmd.cursor.iterator.EndReached && (shortestNote?.isRest while (
? timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) + !osmd.cursor.iterator.EndReached &&
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) < props.timestamp (shortestNote?.isRest
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) < props.timestamp) ? timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) +
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) <
props.timestamp
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) <
props.timestamp)
) { ) {
previousCursorPosition = currentCursorPosition; previousCursorPosition = currentCursorPosition;
osmd.cursor.next(); osmd.cursor.next();
@@ -125,13 +144,15 @@ const PartitionView = (props: PartitionViewProps) => {
// Shamelessly stolen from https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL223C7-L224C1 // Shamelessly stolen from https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL223C7-L224C1
playNotesUnderCursor(); playNotesUnderCursor();
currentCursorPosition = osmd.cursor.cursorElement.offsetLeft; currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
document.getElementById(OSMD_DIV_ID)?.scrollBy(currentCursorPosition - previousCursorPosition, 0) document
.getElementById(OSMD_DIV_ID)
?.scrollBy(currentCursorPosition - previousCursorPosition, 0);
shortestNote = getShortedNoteUnderCursor(); shortestNote = getShortedNoteUnderCursor();
} }
} }
}, [props.timestamp]); }, [props.timestamp]);
return (<div id={OSMD_DIV_ID} style={{ width: '100%', overflow: 'hidden' }} />); return <div id={OSMD_DIV_ID} style={{ width: '100%', overflow: 'hidden' }} />;
} };
export default PartitionView; export default PartitionView;

View File

@@ -1,15 +1,6 @@
import { useTheme, Box, Center } from "native-base"; import React from 'react';
import React from "react";
import { useQuery } from "react-query";
import LoadingComponent, { LoadingView } from "../Loading";
import SlideView from "./SlideView";
import API from "../../API";
type PartitionVisualizerProps = { const PartitionVisualizer = () => {
songId: number;
};
const PartitionVisualizer = ({ songId }: PartitionVisualizerProps) => {
return <></>; return <></>;
}; };

View File

@@ -1,18 +1,9 @@
import { import { Box, Image, Row, Column, Button, Icon } from 'native-base';
useTheme, import IconButton from '../IconButton';
Box, import { MotiView, useDynamicAnimation } from 'moti';
Image, import { Easing } from 'react-native-reanimated';
Row, import React from 'react';
Column, import { FontAwesome5, MaterialCommunityIcons } from '@expo/vector-icons';
ZStack,
Button,
Icon,
} from "native-base";
import IconButton from "../IconButton";
import { MotiView, useDynamicAnimation } from "moti";
import { abs, Easing } from "react-native-reanimated";
import React from "react";
import { FontAwesome5, MaterialCommunityIcons } from "@expo/vector-icons";
type ImgSlideViewProps = { type ImgSlideViewProps = {
sources: [url: string, width: number, height: number][]; sources: [url: string, width: number, height: number][];
@@ -31,6 +22,8 @@ const range = (start: number, end: number, step: number) => {
}; };
const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => { const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
// To get the width, we have to ditch the fst of the tuple
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const totalWidth = sources.reduce((acc, [_, width]) => acc + width, 0); const totalWidth = sources.reduce((acc, [_, width]) => acc + width, 0);
const stepSize = speed / 2; const stepSize = speed / 2;
const stepDuration = 1000 / 2; const stepDuration = 1000 / 2;
@@ -45,7 +38,7 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
animation.animateTo({ animation.animateTo({
translateX: -nbPixelsToSkip, translateX: -nbPixelsToSkip,
transition: { transition: {
type: "timing", type: 'timing',
delay: 0, delay: 0,
easing: Easing.linear, easing: Easing.linear,
}, },
@@ -69,16 +62,11 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
return ( return (
<Column> <Column>
<Box overflow={"hidden"}> <Box overflow={'hidden'}>
<MotiView <MotiView
state={animation} state={animation}
onDidAnimate={( onDidAnimate={(styleProp, didAnimationFinish) => {
styleProp, if (styleProp === 'translateX' && didAnimationFinish) {
didAnimationFinish,
_maybeValue,
{ attemptedValue }
) => {
if (styleProp === "translateX" && didAnimationFinish) {
stepCount++; stepCount++;
} }
}} }}
@@ -102,11 +90,9 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
icon={<Icon as={FontAwesome5} name="play" size="sm" />} icon={<Icon as={FontAwesome5} name="play" size="sm" />}
onPress={() => { onPress={() => {
animation.animateTo({ animation.animateTo({
translateX: range(-totalWidth, 0, stepSize) translateX: range(-totalWidth, 0, stepSize).reverse().slice(stepCount),
.reverse()
.slice(stepCount),
transition: { transition: {
type: "timing", type: 'timing',
easing: Easing.linear, easing: Easing.linear,
duration: stepDuration, duration: stepDuration,
}, },
@@ -120,19 +106,11 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
}} }}
/> />
<IconButton <IconButton
icon={ icon={<Icon as={MaterialCommunityIcons} name="rewind-10" size="sm" />}
<Icon as={MaterialCommunityIcons} name="rewind-10" size="sm" />
}
onPress={() => jumpAt(-200, false)} onPress={() => jumpAt(-200, false)}
/> />
<IconButton <IconButton
icon={ icon={<Icon as={MaterialCommunityIcons} name="fast-forward-10" size="sm" />}
<Icon
as={MaterialCommunityIcons}
name="fast-forward-10"
size="sm"
/>
}
onPress={() => jumpAt(200, false)} onPress={() => jumpAt(200, false)}
/> />
<IconButton <IconButton

View File

@@ -1,36 +1,44 @@
import React from "react"; import React from 'react';
import { translate } from "../i18n/i18n"; import { translate } from '../i18n/i18n';
import { Box, useBreakpointValue, Text, VStack, Progress, Stack, AspectRatio } from 'native-base'; import { Box, Text, VStack, Progress, Stack, AspectRatio } from 'native-base';
import { useNavigation } from "../Navigation"; import { useNavigation } from '../Navigation';
import { Pressable, Image } from "native-base"; import { Image } from 'native-base';
import Card from "../components/Card"; import Card from '../components/Card';
const ProgressBar = ({ xp }: { xp: number}) => { const ProgressBar = ({ xp }: { xp: number }) => {
const level = Math.floor(xp / 1000); const level = Math.floor(xp / 1000);
const nextLevel = level + 1; const nextLevel = level + 1;
const nextLevelThreshold = nextLevel * 1000; const nextLevelThreshold = nextLevel * 1000;
const progessValue = 100 * xp / nextLevelThreshold; const progessValue = (100 * xp) / nextLevelThreshold;
const nav = useNavigation(); const nav = useNavigation();
return ( return (
<Card w="100%" onPress={() => nav.navigate('User')} > <Card w="100%" onPress={() => nav.navigate('User')}>
<Stack padding={4} space={2} direction="row"> <Stack padding={4} space={2} direction="row">
<AspectRatio ratio={1}> <AspectRatio ratio={1}>
<Image position="relative" borderRadius={100} source={{ <Image
uri: "https://wallpaperaccess.com/full/317501.jpg" // TODO : put the actual profile pic position="relative"
}} alt="Profile picture" zIndex={0}/> borderRadius={100}
source={{
uri: 'https://wallpaperaccess.com/full/317501.jpg', // TODO : put the actual profile pic
}}
alt="Profile picture"
zIndex={0}
/>
</AspectRatio> </AspectRatio>
<VStack alignItems={'center'} flexGrow={1} space={2}> <VStack alignItems={'center'} flexGrow={1} space={2}>
<Text>{`${translate('level')} ${level}`}</Text> <Text>{`${translate('level')} ${level}`}</Text>
<Box w="100%"> <Box w="100%">
<Progress value={progessValue} mx="4" /> <Progress value={progessValue} mx="4" />
</Box> </Box>
<Text>{xp} / {nextLevelThreshold} {translate('levelProgress')}</Text> <Text>
{xp} / {nextLevelThreshold} {translate('levelProgress')}
</Text>
</VStack> </VStack>
</Stack> </Stack>
</Card> </Card>
); );
} };
export default ProgressBar; export default ProgressBar;

View File

@@ -1,19 +1,11 @@
import { import { Icon, Input, Button, Flex } from 'native-base';
Icon, import React from 'react';
Input, import { MaterialIcons } from '@expo/vector-icons';
Button, import { translate } from '../i18n/i18n';
Flex} from "native-base"; import { SearchContext } from '../views/SearchView';
import React from "react";
import { MaterialIcons } from "@expo/vector-icons";
import { translate } from "../i18n/i18n";
import { SearchContext } from "../views/SearchView";
import { debounce } from 'lodash'; import { debounce } from 'lodash';
export type Filter = "artist" | "song" | "genre" | "all"; export type Filter = 'artist' | 'song' | 'genre' | 'all';
type SearchBarProps = {
onChangeText?: any;
};
type FilterButton = { type FilterButton = {
name: string; name: string;
@@ -21,9 +13,9 @@ type FilterButton = {
id: Filter; id: Filter;
}; };
const SearchBar = (props: SearchBarProps) => { const SearchBar = () => {
const {filter, updateFilter} = React.useContext(SearchContext); const { filter, updateFilter } = React.useContext(SearchContext);
const {stringQuery, updateStringQuery} = React.useContext(SearchContext); const { stringQuery, updateStringQuery } = React.useContext(SearchContext);
const [barText, updateBarText] = React.useState(stringQuery); const [barText, updateBarText] = React.useState(stringQuery);
const debouncedUpdateStringQuery = debounce(updateStringQuery, 500); const debouncedUpdateStringQuery = debounce(updateStringQuery, 500);
@@ -42,13 +34,13 @@ const SearchBar = (props: SearchBarProps) => {
const handleChangeText = (text: string) => { const handleChangeText = (text: string) => {
debouncedUpdateStringQuery(text); debouncedUpdateStringQuery(text);
updateBarText(text); updateBarText(text);
} };
const filters: FilterButton[] = [ const filters: FilterButton[] = [
{ {
name: translate('allFilter'), name: translate('allFilter'),
callback: () => updateFilter('all'), callback: () => updateFilter('all'),
id: 'all' id: 'all',
}, },
{ {
name: translate('artistFilter'), name: translate('artistFilter'),
@@ -68,37 +60,39 @@ const SearchBar = (props: SearchBarProps) => {
]; ];
return ( return (
<Flex m={3} flexDirection={["column", "row"]}> <Flex m={3} flexDirection={['column', 'row']}>
<Input <Input
onChangeText={(text) => handleChangeText(text)} onChangeText={(text) => handleChangeText(text)}
variant={"rounded"} variant={'rounded'}
value={barText} value={barText}
rounded={"full"} rounded={'full'}
placeholder={translate('search')} placeholder={translate('search')}
width={['100%', '50%']} //responsive array syntax with native-base width={['100%', '50%']} //responsive array syntax with native-base
py={2} py={2}
px={2} px={2}
fontSize={'12'} fontSize={'12'}
InputLeftElement={ InputLeftElement={
<Icon <Icon
m={[1, 2]} m={[1, 2]}
ml={[2, 3]} ml={[2, 3]}
size={['4', '6']} size={['4', '6']}
color="gray.400" color="gray.400"
as={<MaterialIcons name="search" />} as={<MaterialIcons name="search" />}
/> />
}
InputRightElement={
<Icon
m={[1, 2]}
mr={[2, 3]}
size={['4', '6']}
color="gray.400"
onPress={handleClearQuery}
as={<MaterialIcons name="close" />}
/>
} }
InputRightElement={<Icon
m={[1, 2]}
mr={[2, 3]}
size={['4', '6']}
color="gray.400"
onPress={handleClearQuery}
as={<MaterialIcons name="close" />}
/>}
/> />
<Flex flexDirection={'row'} > <Flex flexDirection={'row'}>
{filters.map((btn) => ( {filters.map((btn) => (
<Button <Button
key={btn.name} key={btn.name}
@@ -107,13 +101,14 @@ const SearchBar = (props: SearchBarProps) => {
mx={[2, 5]} mx={[2, 5]}
my={[1, 0]} my={[1, 0]}
minW={[30, 20]} minW={[30, 20]}
variant={filter === btn.id ? 'solid' : 'outline'}> variant={filter === btn.id ? 'solid' : 'outline'}
>
{btn.name} {btn.name}
</Button> </Button>
))} ))}
</Flex> </Flex>
</Flex> </Flex>
); );
} };
export default SearchBar; export default SearchBar;

View File

@@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from "react"; import React from 'react';
import { import {
HStack, HStack,
VStack, VStack,
@@ -12,34 +12,32 @@ import {
useBreakpointValue, useBreakpointValue,
Column, Column,
ScrollView, ScrollView,
} from "native-base"; } from 'native-base';
import { SafeAreaView, useColorScheme } from "react-native"; import { SafeAreaView, useColorScheme } from 'react-native';
import { RootState, useSelector } from "../state/Store"; import { RootState, useSelector } from '../state/Store';
import { SearchContext } from "../views/SearchView"; import { SearchContext } from '../views/SearchView';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { translate } from "../i18n/i18n"; import { translate } from '../i18n/i18n';
import API from "../API"; import API from '../API';
import LoadingComponent from "./Loading"; import LoadingComponent from './Loading';
import ArtistCard from "./ArtistCard"; import ArtistCard from './ArtistCard';
import GenreCard from "./GenreCard"; import GenreCard from './GenreCard';
import SongCard from "./SongCard"; import SongCard from './SongCard';
import CardGridCustom from "./CardGridCustom"; import CardGridCustom from './CardGridCustom';
import TextButton from "./TextButton"; import TextButton from './TextButton';
import SearchHistoryCard from "./HistoryCard"; import SearchHistoryCard from './HistoryCard';
import Song, { SongWithArtist } from "../models/Song"; import Song, { SongWithArtist } from '../models/Song';
import { getSongWArtistSuggestions } from "./utils/api"; import { getSongWArtistSuggestions } from './utils/api';
import { useNavigation } from "../Navigation"; import { useNavigation } from '../Navigation';
const swaToSongCardProps = (song: SongWithArtist) => ({ const swaToSongCardProps = (song: SongWithArtist) => ({
songId: song.id, songId: song.id,
name: song.name, name: song.name,
artistName: song.artist.name, artistName: song.artist.name,
cover: song.cover ?? "https://picsum.photos/200", cover: song.cover ?? 'https://picsum.photos/200',
}); });
const RowCustom = ( const RowCustom = (props: Parameters<typeof Box>[0] & { onPress?: () => void }) => {
props: Parameters<typeof Box>[0] & { onPress?: () => void }
) => {
const settings = useSelector((state: RootState) => state.settings.local); const settings = useSelector((state: RootState) => state.settings.local);
const systemColorMode = useColorScheme(); const systemColorMode = useColorScheme();
const colorScheme = settings.colorScheme; const colorScheme = settings.colorScheme;
@@ -52,13 +50,13 @@ const RowCustom = (
py={3} py={3}
my={1} my={1}
bg={ bg={
(colorScheme == "system" ? systemColorMode : colorScheme) == "dark" (colorScheme == 'system' ? systemColorMode : colorScheme) == 'dark'
? isHovered || isPressed ? isHovered || isPressed
? "gray.800" ? 'gray.800'
: undefined : undefined
: isHovered || isPressed : isHovered || isPressed
? "coolGray.200" ? 'coolGray.200'
: undefined : undefined
} }
> >
{props.children} {props.children}
@@ -75,8 +73,8 @@ type SongRowProps = {
const SongRow = ({ song, onPress }: SongRowProps) => { const SongRow = ({ song, onPress }: SongRowProps) => {
return ( return (
<RowCustom width={"100%"}> <RowCustom width={'100%'}>
<HStack px={2} space={5} justifyContent={"space-between"}> <HStack px={2} space={5} justifyContent={'space-between'}>
<Image <Image
flexShrink={0} flexShrink={0}
flexGrow={0} flexGrow={0}
@@ -87,11 +85,11 @@ const SongRow = ({ song, onPress }: SongRowProps) => {
/> />
<HStack <HStack
style={{ style={{
display: "flex", display: 'flex',
flexShrink: 1, flexShrink: 1,
flexGrow: 1, flexGrow: 1,
alignItems: "center", alignItems: 'center',
justifyContent: "flex-start", justifyContent: 'flex-start',
}} }}
space={6} space={6}
> >
@@ -101,7 +99,7 @@ const SongRow = ({ song, onPress }: SongRowProps) => {
}} }}
isTruncated isTruncated
pl={10} pl={10}
maxW={"100%"} maxW={'100%'}
bold bold
fontSize="md" fontSize="md"
> >
@@ -111,17 +109,17 @@ const SongRow = ({ song, onPress }: SongRowProps) => {
style={{ style={{
flexShrink: 0, flexShrink: 0,
}} }}
fontSize={"sm"} fontSize={'sm'}
> >
{song.artistId ?? "artist"} {song.artistId ?? 'artist'}
</Text> </Text>
</HStack> </HStack>
<TextButton <TextButton
flexShrink={0} flexShrink={0}
flexGrow={0} flexGrow={0}
translate={{ translationKey: "playBtn" }} translate={{ translationKey: 'playBtn' }}
colorScheme="primary" colorScheme="primary"
variant={"outline"} variant={'outline'}
size="sm" size="sm"
onPress={onPress} onPress={onPress}
/> />
@@ -131,26 +129,29 @@ const SongRow = ({ song, onPress }: SongRowProps) => {
}; };
SongRow.defaultProps = { SongRow.defaultProps = {
onPress: () => { }, onPress: () => {},
}; };
const HomeSearchComponent = () => { const HomeSearchComponent = () => {
const { stringQuery, updateStringQuery } = React.useContext(SearchContext); const { updateStringQuery } = React.useContext(SearchContext);
const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery( const { isLoading: isLoadingHistory, data: historyData = [] } = useQuery(
"history", 'history',
() => API.getSearchHistory(0, 12), () => API.getSearchHistory(0, 12),
{ enabled: true } { enabled: true }
); );
const { isLoading: isLoadingSuggestions, data: suggestionsData = [] } = const { isLoading: isLoadingSuggestions, data: suggestionsData = [] } = useQuery(
useQuery("suggestions", () => getSongWArtistSuggestions(), { 'suggestions',
() => getSongWArtistSuggestions(),
{
enabled: true, enabled: true,
}); }
);
return ( return (
<VStack mt="5" style={{ overflow: "hidden" }}> <VStack mt="5" style={{ overflow: 'hidden' }}>
<Card shadow={3} mb={5}> <Card shadow={3} mb={5}>
<Heading margin={5}>{translate("lastSearched")}</Heading> <Heading margin={5}>{translate('lastSearched')}</Heading>
{isLoadingHistory ? ( {isLoadingHistory ? (
<LoadingComponent /> <LoadingComponent />
) : ( ) : (
@@ -169,7 +170,7 @@ const HomeSearchComponent = () => {
)} )}
</Card> </Card>
<Card shadow={3} mt={5} mb={5}> <Card shadow={3} mt={5} mb={5}>
<Heading margin={5}>{translate("songsToGetBetter")}</Heading> <Heading margin={5}>{translate('songsToGetBetter')}</Heading>
{isLoadingSuggestions ? ( {isLoadingSuggestions ? (
<LoadingComponent /> <LoadingComponent />
) : ( ) : (
@@ -183,14 +184,18 @@ const HomeSearchComponent = () => {
); );
}; };
const SongsSearchComponent = (props: any) => { type SongsSearchComponentProps = {
maxRows?: number;
};
const SongsSearchComponent = (props: SongsSearchComponentProps) => {
const { songData } = React.useContext(SearchContext); const { songData } = React.useContext(SearchContext);
const navigation = useNavigation(); const navigation = useNavigation();
return ( return (
<ScrollView> <ScrollView>
<Text fontSize="xl" fontWeight="bold" mt={4}> <Text fontSize="xl" fontWeight="bold" mt={4}>
{translate("songsFilter")} {translate('songsFilter')}
</Text> </Text>
<Box> <Box>
{songData?.length ? ( {songData?.length ? (
@@ -199,94 +204,94 @@ const SongsSearchComponent = (props: any) => {
key={index} key={index}
song={comp} song={comp}
onPress={() => { onPress={() => {
API.createSearchHistoryEntry(comp.name, "song", Date.now()); API.createSearchHistoryEntry(comp.name, 'song');
navigation.navigate("Song", { songId: comp.id }); navigation.navigate('Song', { songId: comp.id });
}} }}
/> />
)) ))
) : ( ) : (
<Text>{translate("errNoResults")}</Text> <Text>{translate('errNoResults')}</Text>
)} )}
</Box> </Box>
</ScrollView> </ScrollView>
); );
}; };
const ArtistSearchComponent = (props: any) => { type ItemSearchComponentProps = {
maxItems?: number;
};
const ArtistSearchComponent = (props: ItemSearchComponentProps) => {
const { artistData } = React.useContext(SearchContext); const { artistData } = React.useContext(SearchContext);
const navigation = useNavigation(); const navigation = useNavigation();
return ( return (
<Box> <Box>
<Text fontSize="xl" fontWeight="bold" mt={4}> <Text fontSize="xl" fontWeight="bold" mt={4}>
{translate("artistFilter")} {translate('artistFilter')}
</Text> </Text>
{artistData?.length ? ( {artistData?.length ? (
<CardGridCustom <CardGridCustom
content={artistData content={artistData.slice(0, props.maxItems ?? artistData.length).map((a) => ({
.slice(0, props?.maxItems ?? artistData.length) image: API.getArtistIllustration(a.id),
.map((a) => ({ name: a.name,
image: API.getArtistIllustration(a.id), id: a.id,
name: a.name, onPress: () => {
id: a.id, API.createSearchHistoryEntry(a.name, 'artist');
onPress: () => { navigation.navigate('Artist', { artistId: a.id });
API.createSearchHistoryEntry(a.name, "artist", Date.now()); },
navigation.navigate("Artist", { artistId: a.id }); }))}
},
}))}
cardComponent={ArtistCard} cardComponent={ArtistCard}
/> />
) : ( ) : (
<Text>{translate("errNoResults")}</Text> <Text>{translate('errNoResults')}</Text>
)} )}
</Box> </Box>
); );
}; };
const GenreSearchComponent = (props: any) => { const GenreSearchComponent = (props: ItemSearchComponentProps) => {
const { genreData } = React.useContext(SearchContext); const { genreData } = React.useContext(SearchContext);
const navigation = useNavigation(); const navigation = useNavigation();
return ( return (
<Box> <Box>
<Text fontSize="xl" fontWeight="bold" mt={4}> <Text fontSize="xl" fontWeight="bold" mt={4}>
{translate("genreFilter")} {translate('genreFilter')}
</Text> </Text>
{genreData?.length ? ( {genreData?.length ? (
<CardGridCustom <CardGridCustom
content={genreData content={genreData.slice(0, props.maxItems ?? genreData.length).map((g) => ({
.slice(0, props?.maxItems ?? genreData.length) image: API.getGenreIllustration(g.id),
.map((g) => ({ name: g.name,
image: API.getGenreIllustration(g.id), id: g.id,
name: g.name, onPress: () => {
id: g.id, API.createSearchHistoryEntry(g.name, 'genre');
onPress: () => { navigation.navigate('Home');
API.createSearchHistoryEntry(g.name, "genre", Date.now()); },
navigation.navigate("Home"); }))}
},
}))}
cardComponent={GenreCard} cardComponent={GenreCard}
/> />
) : ( ) : (
<Text>{translate("errNoResults")}</Text> <Text>{translate('errNoResults')}</Text>
)} )}
</Box> </Box>
); );
}; };
const AllComponent = () => { const AllComponent = () => {
const screenSize = useBreakpointValue({ base: "small", md: "big" }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isMobileView = screenSize == "small"; const isMobileView = screenSize == 'small';
return ( return (
<SafeAreaView> <SafeAreaView>
<Flex <Flex
flexWrap="wrap" flexWrap="wrap"
direction={isMobileView ? "column" : "row"} direction={isMobileView ? 'column' : 'row'}
justifyContent={["flex-start"]} justifyContent={['flex-start']}
mt={4} mt={4}
> >
<Column w={isMobileView ? "100%" : "50%"}> <Column w={isMobileView ? '100%' : '50%'}>
<Box minH={isMobileView ? 100 : 200}> <Box minH={isMobileView ? 100 : 200}>
<ArtistSearchComponent maxItems={6} /> <ArtistSearchComponent maxItems={6} />
</Box> </Box>
@@ -294,7 +299,7 @@ const AllComponent = () => {
<GenreSearchComponent maxItems={6} /> <GenreSearchComponent maxItems={6} />
</Box> </Box>
</Column> </Column>
<Box w={isMobileView ? "100%" : "50%"}> <Box w={isMobileView ? '100%' : '50%'}>
<SongsSearchComponent maxRows={9} /> <SongsSearchComponent maxRows={9} />
</Box> </Box>
</Flex> </Flex>
@@ -311,22 +316,21 @@ const FilterSwitch = () => {
}, [filter]); }, [filter]);
switch (currentFilter) { switch (currentFilter) {
case "all": case 'all':
return <AllComponent />; return <AllComponent />;
case "song": case 'song':
return <SongsSearchComponent />; return <SongsSearchComponent />;
case "artist": case 'artist':
return <ArtistSearchComponent />; return <ArtistSearchComponent />;
case "genre": case 'genre':
return <GenreSearchComponent />; return <GenreSearchComponent />;
default: default:
return <Text>Something very bad happened: {currentFilter}</Text>; return <Text>Something very bad happened: {currentFilter}</Text>;
} }
}; };
export const SearchResultComponent = (props: any) => { export const SearchResultComponent = () => {
const [searchString, setSearchString] = useState<string>(""); const { stringQuery } = React.useContext(SearchContext);
const { stringQuery, updateStringQuery } = React.useContext(SearchContext);
const shouldOutput = !!stringQuery.trim(); const shouldOutput = !!stringQuery.trim();
return shouldOutput ? ( return shouldOutput ? (

View File

@@ -1,8 +1,7 @@
import React from "react"; import React from 'react';
import Card, { CardBorderRadius } from "./Card"; import Card, { CardBorderRadius } from './Card';
import { VStack, Text, Image } from "native-base"; import { VStack, Text, Image } from 'native-base';
import { useNavigation } from "../Navigation"; import { useNavigation } from '../Navigation';
import API from "../API";
type SongCardProps = { type SongCardProps = {
cover: string; cover: string;
name: string; name: string;
@@ -14,12 +13,12 @@ const SongCard = (props: SongCardProps) => {
const { cover, name, artistName, songId } = props; const { cover, name, artistName, songId } = props;
const navigation = useNavigation(); const navigation = useNavigation();
return ( return (
<Card shadow={3} onPress={() => navigation.navigate("Song", { songId })}> <Card shadow={3} onPress={() => navigation.navigate('Song', { songId })}>
<VStack m={1.5} space={3}> <VStack m={1.5} space={3}>
<Image <Image
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }} style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }}
source={{ uri: cover }} source={{ uri: cover }}
alt={[props.name, props.artistName].join("-")} alt={[props.name, props.artistName].join('-')}
/> />
<VStack> <VStack>
<Text isTruncated bold fontSize="md" noOfLines={2} height={50}> <Text isTruncated bold fontSize="md" noOfLines={2} height={50}>

View File

@@ -5,22 +5,24 @@ import { Heading, VStack } from 'native-base';
type SongCardGrid = { type SongCardGrid = {
songs: Parameters<typeof SongCard>[0][]; songs: Parameters<typeof SongCard>[0][];
heading?: JSX.Element, heading?: JSX.Element;
maxItemsPerRow?: number, maxItemsPerRow?: number;
style?: Parameters<typeof FlatGrid>[0]['additionalRowStyle'] style?: Parameters<typeof FlatGrid>[0]['additionalRowStyle'];
} };
const SongCardGrid = (props: SongCardGrid) => { const SongCardGrid = (props: SongCardGrid) => {
return <VStack space={5}> return (
<Heading>{props.heading}</Heading> <VStack space={5}>
<FlatGrid <Heading>{props.heading}</Heading>
maxItemsPerRow={props.maxItemsPerRow} <FlatGrid
additionalRowStyle={props.style ?? { justifyContent: 'flex-start' }} maxItemsPerRow={props.maxItemsPerRow}
data={props.songs} additionalRowStyle={props.style ?? { justifyContent: 'flex-start' }}
renderItem={({ item }) => <SongCard {...item} /> } data={props.songs}
spacing={10} renderItem={({ item }) => <SongCard {...item} />}
/> spacing={10}
</VStack> />
} </VStack>
);
};
export default SongCardGrid; export default SongCardGrid;

View File

@@ -1,24 +1,26 @@
import { Button, Text } from "native-base" import { Button, Text } from 'native-base';
import Translate from "./Translate"; import Translate from './Translate';
import { RequireExactlyOne } from 'type-fest'; import { RequireExactlyOne } from 'type-fest';
type TextButtonProps = Parameters<typeof Button>[0] & RequireExactlyOne<{ type TextButtonProps = Parameters<typeof Button>[0] &
label: string; RequireExactlyOne<{
translate: Parameters<typeof Translate>[0]; label: string;
}> translate: Parameters<typeof Translate>[0];
}>;
const TextButton = (props: TextButtonProps) => { const TextButton = (props: TextButtonProps) => {
// accepts undefined variant, as it is the default variant // accepts undefined variant, as it is the default variant
const textColor = !props.variant || props.variant == 'solid' const textColor = !props.variant || props.variant == 'solid' ? 'light.50' : undefined;
? 'light.50'
: undefined;
return <Button {...props}> return (
{ props.label !== undefined <Button {...props}>
? <Text color={textColor}>{props.label}</Text> {props.label !== undefined ? (
: <Translate color={textColor} {...props.translate} /> <Text color={textColor}>{props.label}</Text>
} ) : (
</Button> <Translate color={textColor} {...props.translate} />
} )}
</Button>
);
};
export default TextButton export default TextButton;

View File

@@ -1,7 +1,7 @@
import { Text } from "native-base"; import { Text } from 'native-base';
import { translate } from "../i18n/i18n"; import { translate } from '../i18n/i18n';
import { en } from "../i18n/Translations"; import { en } from '../i18n/Translations';
import { RootState, useSelector } from "../state/Store"; import { RootState, useSelector } from '../state/Store';
type TranslateProps = { type TranslateProps = {
translationKey: keyof typeof en; translationKey: keyof typeof en;
@@ -9,14 +9,14 @@ type TranslateProps = {
} & Parameters<typeof Text>[0]; } & Parameters<typeof Text>[0];
/** /**
* Translation component * Translation component
* @param param0 * @param param0
* @returns * @returns
*/ */
const Translate = ({ translationKey, format, ...props }: TranslateProps) => { const Translate = ({ translationKey, format, ...props }: TranslateProps) => {
const selectedLanguage = useSelector((state: RootState) => state.language.value); const selectedLanguage = useSelector((state: RootState) => state.language.value);
const translated = translate(translationKey, selectedLanguage); const translated = translate(translationKey, selectedLanguage);
return <Text {...props}>{format ? format(translated) : translated}</Text>; return <Text {...props}>{format ? format(translated) : translated}</Text>;
} };
export default Translate; export default Translate;

View File

@@ -5,9 +5,9 @@ import {
octaveKeys, octaveKeys,
isAccidental, isAccidental,
HighlightedKey, HighlightedKey,
} from "../../models/Piano"; } from '../../models/Piano';
import { Box, Row, Text } from "native-base"; import { Box, Row, Text } from 'native-base';
import PianoKeyComp from "./PianoKeyComp"; import PianoKeyComp from './PianoKeyComp';
type OctaveProps = Parameters<typeof Box>[0] & { type OctaveProps = Parameters<typeof Box>[0] & {
number: number; number: number;
@@ -57,22 +57,20 @@ const Octave = (props: OctaveProps) => {
const whiteKeys = keys.filter((k) => !isAccidental(k)); const whiteKeys = keys.filter((k) => !isAccidental(k));
const blackKeys = keys.filter(isAccidental); const blackKeys = keys.filter(isAccidental);
const whiteKeyWidthExpr = "calc(100% / 7)"; const whiteKeyWidthExpr = 'calc(100% / 7)';
const whiteKeyHeightExpr = "100%"; const whiteKeyHeightExpr = '100%';
const blackKeyWidthExpr = "calc(100% / 13)"; const blackKeyWidthExpr = 'calc(100% / 13)';
const blackKeyHeightExpr = "calc(100% / 1.5)"; const blackKeyHeightExpr = 'calc(100% / 1.5)';
return ( return (
<Box {...props}> <Box {...props}>
<Row height={"100%"} width={"100%"}> <Row height={'100%'} width={'100%'}>
{whiteKeys.map((key, i) => { {whiteKeys.map((key) => {
const highlightedKey = highlightedNotes.find( const highlightedKey = highlightedNotes.find((h) => h.key.note === key.note);
(h) => h.key.note === key.note
);
const isHighlighted = highlightedKey !== undefined; const isHighlighted = highlightedKey !== undefined;
const highlightColor = const highlightColor = highlightedKey?.bgColor ?? defaultHighlightColor;
highlightedKey?.bgColor ?? defaultHighlightColor;
return ( return (
// eslint-disable-next-line react/jsx-key
<PianoKeyComp <PianoKeyComp
pianoKey={key} pianoKey={key}
showNoteName={showNoteNames} showNoteName={showNoteNames}
@@ -89,13 +87,11 @@ const Octave = (props: OctaveProps) => {
); );
})} })}
{blackKeys.map((key, i) => { {blackKeys.map((key, i) => {
const highlightedKey = highlightedNotes.find( const highlightedKey = highlightedNotes.find((h) => h.key.note === key.note);
(h) => h.key.note === key.note
);
const isHighlighted = highlightedKey !== undefined; const isHighlighted = highlightedKey !== undefined;
const highlightColor = const highlightColor = highlightedKey?.bgColor ?? defaultHighlightColor;
highlightedKey?.bgColor ?? defaultHighlightColor;
return ( return (
// eslint-disable-next-line react/jsx-key
<PianoKeyComp <PianoKeyComp
pianoKey={key} pianoKey={key}
showNoteName={showNoteNames} showNoteName={showNoteNames}
@@ -107,15 +103,15 @@ const Octave = (props: OctaveProps) => {
style={{ style={{
width: blackKeyWidthExpr, width: blackKeyWidthExpr,
height: blackKeyHeightExpr, height: blackKeyHeightExpr,
position: "absolute", position: 'absolute',
left: `calc(calc(${whiteKeyWidthExpr} * ${ left: `calc(calc(${whiteKeyWidthExpr} * ${
i + ((i > 1) as unknown as number) + 1 i + ((i > 1) as unknown as number) + 1
}) - calc(${blackKeyWidthExpr} / 2))`, }) - calc(${blackKeyWidthExpr} / 2))`,
top: "0px", top: '0px',
}} }}
text={{ text={{
color: "white", color: 'white',
fontSize: "xs", fontSize: 'xs',
}} }}
/> />
); );
@@ -139,18 +135,18 @@ const Octave = (props: OctaveProps) => {
}; };
Octave.defaultProps = { Octave.defaultProps = {
startNote: "C", startNote: 'C',
endNote: "B", endNote: 'B',
showNoteNames: "onpress", showNoteNames: 'onpress',
showOctaveNumber: false, showOctaveNumber: false,
whiteKeyBg: "white", whiteKeyBg: 'white',
whiteKeyBgPressed: "gray.200", whiteKeyBgPressed: 'gray.200',
whiteKeyBgHovered: "gray.100", whiteKeyBgHovered: 'gray.100',
blackKeyBg: "black", blackKeyBg: 'black',
blackKeyBgPressed: "gray.600", blackKeyBgPressed: 'gray.600',
blackKeyBgHovered: "gray.700", blackKeyBgHovered: 'gray.700',
highlightedNotes: [], highlightedNotes: [],
defaultHighlightColor: "#FF0000", defaultHighlightColor: '#FF0000',
onNoteDown: () => {}, onNoteDown: () => {},
onNoteUp: () => {}, onNoteUp: () => {},
}; };

View File

@@ -1,11 +1,6 @@
import { Box, Pressable, Text } from "native-base"; import { Box, Pressable, Text } from 'native-base';
import { StyleProp, ViewStyle } from "react-native"; import { StyleProp, ViewStyle } from 'react-native';
import { import { PianoKey, NoteNameBehavior, octaveKeys, keyToStr } from '../../models/Piano';
PianoKey,
NoteNameBehavior,
octaveKeys,
keyToStr,
} from "../../models/Piano";
type PianoKeyProps = { type PianoKeyProps = {
pianoKey: PianoKey; pianoKey: PianoKey;
@@ -48,22 +43,18 @@ const PianoKeyComp = ({
}: PianoKeyProps) => { }: PianoKeyProps) => {
const textDefaultProps = { const textDefaultProps = {
style: { style: {
userSelect: "none", userSelect: 'none',
WebkitUserSelect: "none", WebkitUserSelect: 'none',
MozUserSelect: "none", MozUserSelect: 'none',
msUserSelect: "none", msUserSelect: 'none',
}, },
fontSize: "xl", fontSize: 'xl',
color: "black", color: 'black',
} as Parameters<typeof Text>[0]; } as Parameters<typeof Text>[0];
const textProps = { ...textDefaultProps, ...text }; const textProps = { ...textDefaultProps, ...text };
return ( return (
<Pressable <Pressable onPressIn={onKeyDown} onPressOut={onKeyUp} style={style}>
onPressIn={onKeyDown}
onPressOut={onKeyUp}
style={style}
>
{({ isHovered, isPressed }) => ( {({ isHovered, isPressed }) => (
<Box <Box
bg={(() => { bg={(() => {
@@ -90,9 +81,9 @@ const PianoKeyComp = ({
PianoKeyComp.defaultProps = { PianoKeyComp.defaultProps = {
key: octaveKeys[0], key: octaveKeys[0],
showNoteNames: NoteNameBehavior.onhover, showNoteNames: NoteNameBehavior.onhover,
keyBg: "white", keyBg: 'white',
keyBgPressed: "gray.200", keyBgPressed: 'gray.200',
keyBgHovered: "gray.100", keyBgHovered: 'gray.100',
onKeyDown: () => {}, onKeyDown: () => {},
onKeyUp: () => {}, onKeyUp: () => {},
text: {}, text: {},

View File

@@ -1,16 +1,8 @@
import { Row, Box } from "native-base"; import { Row } from 'native-base';
import React, { useState, useEffect } from "react"; import React from 'react';
import Octave from "./Octave"; import Octave from './Octave';
import { StyleProp, ViewStyle } from "react-native"; import { StyleProp, ViewStyle } from 'react-native';
import { import { Note, PianoKey, NoteNameBehavior, HighlightedKey } from '../../models/Piano';
Note,
PianoKey,
NoteNameBehavior,
KeyPressStyle,
keyToStr,
strToKey,
HighlightedKey,
} from "../../models/Piano";
type VirtualPianoProps = Parameters<typeof Row>[0] & { type VirtualPianoProps = Parameters<typeof Row>[0] & {
onNoteDown: (note: PianoKey) => void; onNoteDown: (note: PianoKey) => void;
@@ -37,29 +29,21 @@ const VirtualPiano = ({
showOctaveNumbers, showOctaveNumbers,
style, style,
}: VirtualPianoProps) => { }: VirtualPianoProps) => {
const notesList: Array<Note> = [ const notesList: Array<Note> = [Note.C, Note.D, Note.E, Note.F, Note.G, Note.A, Note.B];
Note.C,
Note.D,
Note.E,
Note.F,
Note.G,
Note.A,
Note.B,
];
const octaveList = []; const octaveList = [];
for (let octaveNum = startOctave; octaveNum <= endOctave; octaveNum++) { for (let octaveNum = startOctave; octaveNum <= endOctave; octaveNum++) {
octaveList.push(octaveNum); octaveList.push(octaveNum);
} }
const octaveWidthExpr = `calc(100% / ${octaveList.length})`; const octaveWidthExpr = `calc(100% / ${octaveList.length})`;
return ( return (
<Row style={style}> <Row style={style}>
{octaveList.map((octaveNum) => { {octaveList.map((octaveNum) => {
return ( return (
<Octave <Octave
style={{ width: octaveWidthExpr, height: "100%" }} style={{ width: octaveWidthExpr, height: '100%' }}
key={octaveNum} key={octaveNum}
number={octaveNum} number={octaveNum}
showNoteNames={showNoteNames} showNoteNames={showNoteNames}
@@ -68,9 +52,7 @@ const VirtualPiano = ({
n.key.octave ? n.key.octave == octaveNum : true n.key.octave ? n.key.octave == octaveNum : true
)} )}
startNote={octaveNum == startOctave ? startNote : notesList[0]} startNote={octaveNum == startOctave ? startNote : notesList[0]}
endNote={ endNote={octaveNum == endOctave ? endNote : notesList[notesList.length - 1]}
octaveNum == endOctave ? endNote : notesList[notesList.length - 1]
}
onNoteDown={onNoteDown} onNoteDown={onNoteDown}
onNoteUp={onNoteUp} onNoteUp={onNoteUp}
/> />
@@ -81,8 +63,8 @@ const VirtualPiano = ({
}; };
VirtualPiano.defaultProps = { VirtualPiano.defaultProps = {
onNoteDown: (_n: PianoKey) => {}, onNoteDown: () => {},
onNoteUp: (_n: PianoKey) => {}, onNoteUp: () => {},
startOctave: 2, startOctave: 2,
startNote: Note.C, startNote: Note.C,
endOctave: 6, endOctave: 6,
@@ -90,7 +72,7 @@ VirtualPiano.defaultProps = {
showNoteNames: NoteNameBehavior.onpress, showNoteNames: NoteNameBehavior.onpress,
highlightedNotes: [], highlightedNotes: [],
showOctaveNumbers: true, showOctaveNumbers: true,
style: {}, style: {},
}; };
export default VirtualPiano; export default VirtualPiano;

View File

@@ -1,37 +1,26 @@
import React from "react"; import React from 'react';
import { translate } from "../../i18n/i18n"; import { translate } from '../../i18n/i18n';
import { string } from "yup"; import { string } from 'yup';
import { import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base';
FormControl,
Input,
Stack,
WarningOutlineIcon,
Box,
Button,
useToast,
} from "native-base";
interface ChangeEmailFormProps { interface ChangeEmailFormProps {
onSubmit: ( onSubmit: (oldEmail: string, newEmail: string) => Promise<string>;
oldEmail: string,
newEmail: string
) => Promise<string>;
} }
const validationSchemas = { const validationSchemas = {
email: string().email("Invalid email").required("Email is required"), email: string().email('Invalid email').required('Email is required'),
}; };
const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => { const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
const [formData, setFormData] = React.useState({ const [formData, setFormData] = React.useState({
oldEmail: { oldEmail: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
newEmail: { newEmail: {
value: "", value: '',
error: null as string | null, error: null as string | null,
} },
}); });
const [submittingForm, setSubmittingForm] = React.useState(false); const [submittingForm, setSubmittingForm] = React.useState(false);
@@ -42,77 +31,73 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}> <Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl <FormControl
isRequired isRequired
isInvalid={ isInvalid={formData.oldEmail.error !== null || formData.newEmail.error !== null}
formData.oldEmail.error !== null ||
formData.newEmail.error !== null
}
> >
<FormControl.Label>{translate("oldEmail")}</FormControl.Label> <FormControl.Label>{translate('oldEmail')}</FormControl.Label>
<Input <Input
isRequired isRequired
type="text" type="text"
placeholder={translate("oldEmail")} placeholder={translate('oldEmail')}
value={formData.oldEmail.value} value={formData.oldEmail.value}
onChangeText={(t) => { onChangeText={(t) => {
let error: null | string = null; let error: null | string = null;
validationSchemas.email validationSchemas.email
.validate(t) .validate(t)
.catch((e) => (error = e.message)) .catch((e) => (error = e.message))
.finally(() => { .finally(() => {
setFormData({ ...formData, oldEmail: { value: t, error } }); setFormData({ ...formData, oldEmail: { value: t, error } });
}); });
}} }}
/> />
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} > <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.oldEmail.error} {formData.oldEmail.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<FormControl.Label>{translate("newEmail")}</FormControl.Label> <FormControl.Label>{translate('newEmail')}</FormControl.Label>
<Input <Input
isRequired isRequired
type="text" type="text"
placeholder={translate("newEmail")} placeholder={translate('newEmail')}
value={formData.newEmail.value} value={formData.newEmail.value}
onChangeText={(t) => { onChangeText={(t) => {
let error: null | string = null; let error: null | string = null;
validationSchemas.email validationSchemas.email
.validate(t) .validate(t)
.catch((e) => (error = e.message)) .catch((e) => (error = e.message))
.finally(() => { .finally(() => {
setFormData({ ...formData, newEmail: { value: t, error } }); setFormData({ ...formData, newEmail: { value: t, error } });
}); });
}} }}
/> />
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} > <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.oldEmail.error} {formData.oldEmail.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<Button <Button
style={{ marginTop: 10 }} style={{ marginTop: 10 }}
isLoading={submittingForm} isLoading={submittingForm}
isDisabled={ isDisabled={formData.newEmail.error !== null}
formData.newEmail.error !== null onPress={async () => {
} setSubmittingForm(true);
onPress={async () => { try {
setSubmittingForm(true); const resp = await onSubmit(
try { formData.oldEmail.value,
const resp = await onSubmit(formData.oldEmail.value, formData.newEmail.value
formData.newEmail.value );
); toast.show({ description: resp });
toast.show({ description: resp }); } catch (e) {
} catch (e) { toast.show({ description: e as string });
toast.show({ description: e as string }); } finally {
} finally { setSubmittingForm(false);
setSubmittingForm(false); }
} }}
}} >
> {translate('submitBtn')}
{translate("submitBtn")} </Button>
</Button>
</FormControl> </FormControl>
</Stack> </Stack>
</Box> </Box>
); );
} };
export default ChangeEmailForm; export default ChangeEmailForm;

View File

@@ -1,36 +1,24 @@
import React from "react"; import React from 'react';
import { translate } from "../../i18n/i18n"; import { translate } from '../../i18n/i18n';
import { string } from "yup"; import { string } from 'yup';
import { import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base';
FormControl,
Input,
Stack,
WarningOutlineIcon,
Box,
Button,
useToast,
} from "native-base";
interface ChangePasswordFormProps { interface ChangePasswordFormProps {
onSubmit: ( onSubmit: (oldPassword: string, newPassword: string) => Promise<string>;
oldPassword: string,
newPassword: string
) => Promise<string>;
} }
const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => { const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
const [formData, setFormData] = React.useState({ const [formData, setFormData] = React.useState({
oldPassword: { oldPassword: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
newPassword: { newPassword: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
confirmNewPassword: { confirmNewPassword: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
}); });
@@ -38,120 +26,119 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
const validationSchemas = { const validationSchemas = {
password: string() password: string()
.min(4, translate("passwordTooShort")) .min(4, translate('passwordTooShort'))
.max(100, translate("passwordTooLong")) .max(100, translate('passwordTooLong'))
.required("Password is required"), .required('Password is required'),
}; };
const toast = useToast(); const toast = useToast();
return ( return (
<Box> <Box>
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}> <Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl <FormControl
isRequired isRequired
isInvalid={ isInvalid={
formData.oldPassword.error !== null || formData.oldPassword.error !== null ||
formData.newPassword.error !== null || formData.newPassword.error !== null ||
formData.confirmNewPassword.error !== null} formData.confirmNewPassword.error !== null
}
> >
<FormControl.Label>{translate('oldPassword')}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate('oldPassword')}
value={formData.oldPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, oldPassword: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.oldPassword.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("oldPassword")}</FormControl.Label> <FormControl.Label>{translate('newPassword')}</FormControl.Label>
<Input <Input
isRequired isRequired
type="password" type="password"
placeholder={translate("oldPassword")} placeholder={translate('newPassword')}
value={formData.oldPassword.value} value={formData.newPassword.value}
onChangeText={(t) => { onChangeText={(t) => {
let error: null | string = null; let error: null | string = null;
validationSchemas.password validationSchemas.password
.validate(t) .validate(t)
.catch((e) => (error = e.message)) .catch((e) => (error = e.message))
.finally(() => { .finally(() => {
setFormData({ ...formData, oldPassword: { value: t, error } }); setFormData({ ...formData, newPassword: { value: t, error } });
}); });
}} }}
/> />
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} > <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.oldPassword.error} {formData.newPassword.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<FormControl.Label>{translate("newPassword")}</FormControl.Label> <FormControl.Label>{translate('confirmNewPassword')}</FormControl.Label>
<Input <Input
isRequired isRequired
type="password" type="password"
placeholder={translate("newPassword")} placeholder={translate('confirmNewPassword')}
value={formData.newPassword.value} value={formData.confirmNewPassword.value}
onChangeText={(t) => { onChangeText={(t) => {
let error: null | string = null; let error: null | string = null;
validationSchemas.password validationSchemas.password
.validate(t) .validate(t)
.catch((e) => (error = e.message)) .catch((e) => (error = e.message));
.finally(() => {
setFormData({ ...formData, newPassword: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} >
{formData.newPassword.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("confirmNewPassword")}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("confirmNewPassword")}
value={formData.confirmNewPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
if (!error && t !== formData.newPassword.value) { if (!error && t !== formData.newPassword.value) {
error = translate("passwordsDontMatch"); error = translate('passwordsDontMatch');
} }
setFormData({ setFormData({
...formData, ...formData,
confirmNewPassword: { value: t, error }, confirmNewPassword: { value: t, error },
}); });
}} }}
/> />
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} > <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.confirmNewPassword.error} {formData.confirmNewPassword.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<Button <Button
style={{ marginTop: 10 }} style={{ marginTop: 10 }}
isLoading={submittingForm} isLoading={submittingForm}
isDisabled={ isDisabled={
formData.oldPassword.error !== null || formData.oldPassword.error !== null ||
formData.newPassword.error !== null || formData.newPassword.error !== null ||
formData.confirmNewPassword.error !== null || formData.confirmNewPassword.error !== null ||
formData.oldPassword.value === "" || formData.oldPassword.value === '' ||
formData.newPassword.value === "" || formData.newPassword.value === '' ||
formData.confirmNewPassword.value === "" formData.confirmNewPassword.value === ''
}
onPress={async () => {
setSubmittingForm(true);
try {
const resp = await onSubmit(
formData.oldPassword.value,
formData.newPassword.value
);
toast.show({ description: resp });
} catch (e) {
toast.show({ description: e as string });
} finally {
setSubmittingForm(false);
} }
}} onPress={async () => {
> setSubmittingForm(true);
{translate("submitBtn")} try {
</Button> const resp = await onSubmit(
formData.oldPassword.value,
formData.newPassword.value
);
toast.show({ description: resp });
} catch (e) {
toast.show({ description: e as string });
} finally {
setSubmittingForm(false);
}
}}
>
{translate('submitBtn')}
</Button>
</FormControl> </FormControl>
</Stack> </Stack>
</Box> </Box>
); );
} };
export default ChangePasswordForm; export default ChangePasswordForm;

View File

@@ -1,17 +1,10 @@
// a form for sign in // a form for sign in
import React from "react"; import React from 'react';
import { Translate, translate } from "../../i18n/i18n"; import { Translate, translate } from '../../i18n/i18n';
import { string } from "yup"; import { string } from 'yup';
import { import { FormControl, Input, Stack, WarningOutlineIcon, Box, useToast } from 'native-base';
FormControl, import TextButton from '../TextButton';
Input,
Stack,
WarningOutlineIcon,
Box,
useToast,
} from "native-base";
import TextButton from "../TextButton";
interface SigninFormProps { interface SigninFormProps {
onSubmit: (username: string, password: string) => Promise<string>; onSubmit: (username: string, password: string) => Promise<string>;
@@ -20,11 +13,11 @@ interface SigninFormProps {
const LoginForm = ({ onSubmit }: SigninFormProps) => { const LoginForm = ({ onSubmit }: SigninFormProps) => {
const [formData, setFormData] = React.useState({ const [formData, setFormData] = React.useState({
username: { username: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
password: { password: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
}); });
@@ -32,98 +25,96 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
const validationSchemas = { const validationSchemas = {
username: string() username: string()
.min(3, translate("usernameTooShort")) .min(3, translate('usernameTooShort'))
.max(20, translate("usernameTooLong")) .max(20, translate('usernameTooLong'))
.required("Username is required"), .required('Username is required'),
password: string() password: string()
.min(4, translate("passwordTooShort")) .min(4, translate('passwordTooShort'))
.max(100, translate("passwordTooLong")) .max(100, translate('passwordTooLong'))
.required("Password is required"), .required('Password is required'),
}; };
const toast = useToast(); const toast = useToast();
return ( return (
<Box alignItems="center" style={{ width: '100%' }}> <Box alignItems="center" style={{ width: '100%' }}>
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}> <Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl <FormControl
isRequired
isInvalid={formData.username.error !== null || formData.password.error !== null}
>
<FormControl.Label>
<Translate translationKey="username" />
</FormControl.Label>
<Input
isRequired isRequired
isInvalid={ type="text"
placeholder="Username"
autoComplete="username"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey="password" />
</FormControl.Label>
<Input
isRequired
type="password"
autoComplete="password"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.password.error}
</FormControl.ErrorMessage>
<TextButton
translate={{ translationKey: 'signInBtn' }}
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={
formData.password.error !== null ||
formData.username.error !== null || formData.username.error !== null ||
formData.password.error !== null formData.username.value === '' ||
formData.password.value === ''
} }
> onPress={async () => {
<FormControl.Label> setSubmittingForm(true);
<Translate translationKey='username'/> try {
</FormControl.Label> const resp = await onSubmit(
<Input formData.username.value,
isRequired formData.password.value
type="text" );
placeholder="Username" toast.show({ description: resp, colorScheme: 'secondary' });
autoComplete="username" setSubmittingForm(false);
value={formData.username.value} } catch (e) {
onChangeText={(t) => { toast.show({
let error: null | string = null; description: e as string,
validationSchemas.username colorScheme: 'red',
.validate(t) avoidKeyboard: true,
.catch((e) => (error = e.message)) });
.finally(() => { setSubmittingForm(false);
setFormData({ ...formData, username: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage
leftIcon={<WarningOutlineIcon size="xs" />}
>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey='password'/>
</FormControl.Label>
<Input
isRequired
type="password"
autoComplete="password"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage
leftIcon={<WarningOutlineIcon size="xs" />}
>
{formData.password.error}
</FormControl.ErrorMessage>
<TextButton translate={{ translationKey: 'signInBtn' }}
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
formData.username.value === "" ||
formData.password.value === ""
} }
onPress={async () => { }}
setSubmittingForm(true); />
try { </FormControl>
const resp = await onSubmit( </Stack>
formData.username.value,
formData.password.value
);
toast.show({ description: resp, colorScheme: 'secondary' });
setSubmittingForm(false);
} catch (e) {
toast.show({ description: e as string, colorScheme: 'red', avoidKeyboard: true })
setSubmittingForm(false);
}
}}
/>
</FormControl>
</Stack>
</Box> </Box>
); );
}; };

View File

@@ -1,42 +1,31 @@
// a form for sign up // a form for sign up
import React from "react"; import React from 'react';
import { Translate, translate } from "../../i18n/i18n"; import { Translate, translate } from '../../i18n/i18n';
import { string } from "yup"; import { string } from 'yup';
import { import { FormControl, Input, Stack, WarningOutlineIcon, Box, useToast } from 'native-base';
FormControl, import TextButton from '../TextButton';
Input,
Stack,
WarningOutlineIcon,
Box,
useToast,
} from "native-base";
import TextButton from "../TextButton";
interface SignupFormProps { interface SignupFormProps {
onSubmit: ( onSubmit: (username: string, password: string, email: string) => Promise<string>;
username: string,
password: string,
email: string
) => Promise<string>;
} }
const SignUpForm = ({ onSubmit }: SignupFormProps) => { const SignUpForm = ({ onSubmit }: SignupFormProps) => {
const [formData, setFormData] = React.useState({ const [formData, setFormData] = React.useState({
username: { username: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
password: { password: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
repeatPassword: { repeatPassword: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
email: { email: {
value: "", value: '',
error: null as string | null, error: null as string | null,
}, },
}); });
@@ -44,20 +33,20 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
const validationSchemas = { const validationSchemas = {
username: string() username: string()
.min(3, translate("usernameTooShort")) .min(3, translate('usernameTooShort'))
.max(20, translate("usernameTooLong")) .max(20, translate('usernameTooLong'))
.required("Username is required"), .required('Username is required'),
email: string().email("Invalid email").required("Email is required"), email: string().email('Invalid email').required('Email is required'),
password: string() password: string()
.min(4, translate("passwordTooShort")) .min(4, translate('passwordTooShort'))
.max(100, translate("passwordTooLong")) .max(100, translate('passwordTooLong'))
// .matches( // .matches(
// /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$-_%\^&\*])(?=.{8,})/, // /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$-_%\^&\*])(?=.{8,})/,
// translate( // translate(
// "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character" // "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character"
// ) // )
// ) // )
.required("Password is required"), .required('Password is required'),
}; };
const toast = useToast(); const toast = useToast();
@@ -75,7 +64,7 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
} }
> >
<FormControl.Label> <FormControl.Label>
<Translate translationKey='username'/> <Translate translationKey="username" />
</FormControl.Label> </FormControl.Label>
<Input <Input
isRequired isRequired
@@ -93,13 +82,11 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
}); });
}} }}
/> />
<FormControl.ErrorMessage <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
leftIcon={<WarningOutlineIcon size="xs" />}
>
{formData.username.error} {formData.username.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<FormControl.Label> <FormControl.Label>
<Translate translationKey='email'/> <Translate translationKey="email" />
</FormControl.Label> </FormControl.Label>
<Input <Input
isRequired isRequired
@@ -117,13 +104,11 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
}); });
}} }}
/> />
<FormControl.ErrorMessage <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
leftIcon={<WarningOutlineIcon size="xs" />}
>
{formData.email.error} {formData.email.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<FormControl.Label> <FormControl.Label>
<Translate translationKey='password'/> <Translate translationKey="password" />
</FormControl.Label> </FormControl.Label>
<Input <Input
isRequired isRequired
@@ -140,13 +125,11 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
}); });
}} }}
/> />
<FormControl.ErrorMessage <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
leftIcon={<WarningOutlineIcon size="xs" />}
>
{formData.password.error} {formData.password.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<FormControl.Label> <FormControl.Label>
<Translate translationKey='repeatPassword'/> <Translate translationKey="repeatPassword" />
</FormControl.Label> </FormControl.Label>
<Input <Input
isRequired isRequired
@@ -160,7 +143,7 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
.catch((e) => (error = e.message)) .catch((e) => (error = e.message))
.finally(() => { .finally(() => {
if (!error && t !== formData.password.value) { if (!error && t !== formData.password.value) {
error = translate("passwordsDontMatch"); error = translate('passwordsDontMatch');
} }
setFormData({ setFormData({
...formData, ...formData,
@@ -169,12 +152,11 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
}); });
}} }}
/> />
<FormControl.ErrorMessage <FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
leftIcon={<WarningOutlineIcon size="xs" />}
>
{formData.repeatPassword.error} {formData.repeatPassword.error}
</FormControl.ErrorMessage> </FormControl.ErrorMessage>
<TextButton translate={{ translationKey: 'signUpBtn' }} <TextButton
translate={{ translationKey: 'signUpBtn' }}
style={{ marginTop: 10 }} style={{ marginTop: 10 }}
isLoading={submittingForm} isLoading={submittingForm}
isDisabled={ isDisabled={
@@ -182,10 +164,10 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
formData.username.error !== null || formData.username.error !== null ||
formData.repeatPassword.error !== null || formData.repeatPassword.error !== null ||
formData.email.error !== null || formData.email.error !== null ||
formData.username.value === "" || formData.username.value === '' ||
formData.password.value === "" || formData.password.value === '' ||
formData.repeatPassword.value === "" || formData.repeatPassword.value === '' ||
formData.repeatPassword.value === "" formData.repeatPassword.value === ''
} }
onPress={async () => { onPress={async () => {
setSubmittingForm(true); setSubmittingForm(true);

View File

@@ -1,16 +1,7 @@
import * as React from "react"; import * as React from 'react';
import { StyleProp, ViewStyle, StyleSheet } from "react-native"; import { StyleProp, ViewStyle } from 'react-native';
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from '@expo/vector-icons';
import { import { View, Text, Row, Icon, Button, useBreakpointValue } from 'native-base';
View,
Text,
Pressable,
Box,
Row,
Icon,
Button,
useBreakpointValue,
} from "native-base";
import { import {
createNavigatorFactory, createNavigatorFactory,
DefaultNavigatorOptions, DefaultNavigatorOptions,
@@ -21,12 +12,12 @@ import {
TabRouter, TabRouter,
TabRouterOptions, TabRouterOptions,
useNavigationBuilder, useNavigationBuilder,
} from "@react-navigation/native"; } from '@react-navigation/native';
import { useNavigation } from "../../Navigation"; import { useNavigation } from '../../Navigation';
const TabRowNavigatorInitialComponentName = "TabIndex"; const TabRowNavigatorInitialComponentName = 'TabIndex';
export {TabRowNavigatorInitialComponentName}; export { TabRowNavigatorInitialComponentName };
// Props accepted by the view // Props accepted by the view
type TabNavigationConfig = { type TabNavigationConfig = {
@@ -37,6 +28,7 @@ type TabNavigationConfig = {
// Supported screen options // Supported screen options
type TabNavigationOptions = { type TabNavigationOptions = {
title?: string; title?: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
iconProvider?: any; iconProvider?: any;
iconName?: string; iconName?: string;
}; };
@@ -70,34 +62,31 @@ function TabNavigator({
contentStyle, contentStyle,
}: Props) { }: Props) {
const navigator = useNavigation(); const navigator = useNavigation();
const { state, navigation, descriptors, NavigationContent } = const { state, navigation, descriptors, NavigationContent } = useNavigationBuilder<
useNavigationBuilder< TabNavigationState<ParamListBase>,
TabNavigationState<ParamListBase>, TabRouterOptions,
TabRouterOptions, TabActionHelpers<ParamListBase>,
TabActionHelpers<ParamListBase>, TabNavigationOptions,
TabNavigationOptions, TabNavigationEventMap
TabNavigationEventMap >(TabRouter, {
>(TabRouter, { children,
children, screenOptions,
screenOptions, initialRouteName,
initialRouteName, });
});
const screenSize = useBreakpointValue({ base: "small", md: "big" }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const [isPanelView, setIsPanelView] = React.useState(false); const [isPanelView, setIsPanelView] = React.useState(false);
const isMobileView = screenSize == "small"; const isMobileView = screenSize == 'small';
React.useEffect(() => { React.useEffect(() => {
if (state.index === 0) { if (state.index === 0) {
if (isMobileView) { if (isMobileView) {
setIsPanelView(true); setIsPanelView(true);
} else { } else {
navigation.reset( navigation.reset({
{ ...state,
...state, index: 1,
index: 1, });
}
);
} }
} }
}, [state.index]); }, [state.index]);
@@ -110,18 +99,18 @@ function TabNavigator({
return ( return (
<NavigationContent> <NavigationContent>
<Row height={"100%"}> <Row height={'100%'}>
{(!isMobileView || isPanelView) && ( {(!isMobileView || isPanelView) && (
<View <View
style={[ style={[
{ {
display: "flex", display: 'flex',
flexDirection: "column", flexDirection: 'column',
justifyContent: "flex-start", justifyContent: 'flex-start',
borderRightWidth: 1, borderRightWidth: 1,
borderRightColor: "lightgray", borderRightColor: 'lightgray',
overflow: "scroll", overflow: 'scroll',
width: isMobileView ? "100%" : "clamp(200px, 20%, 300px)", width: isMobileView ? '100%' : 'clamp(200px, 20%, 300px)',
}, },
tabBarStyle, tabBarStyle,
]} ]}
@@ -135,11 +124,11 @@ function TabNavigator({
return ( return (
<Button <Button
variant={"ghost"} variant={'ghost'}
key={route.key} key={route.key}
onPress={() => { onPress={() => {
const event = navigation.emit({ const event = navigation.emit({
type: "tabPress", type: 'tabPress',
target: route.key, target: route.key,
canPreventDefault: true, canPreventDefault: true,
data: { data: {
@@ -157,12 +146,16 @@ function TabNavigator({
setIsPanelView(false); setIsPanelView(false);
} }
}} }}
bgColor={isSelected && (!isMobileView || !isPanelView) ? "primary.300" : undefined} bgColor={
isSelected && (!isMobileView || !isPanelView)
? 'primary.300'
: undefined
}
style={{ style={{
justifyContent: "flex-start", justifyContent: 'flex-start',
padding: "10px", padding: '10px',
height: "50px", height: '50px',
width: "100%", width: '100%',
}} }}
leftIcon={ leftIcon={
options?.iconProvider && options?.iconName ? ( options?.iconProvider && options?.iconName ? (
@@ -185,17 +178,14 @@ function TabNavigator({
)} )}
{(!isMobileView || !isPanelView) && ( {(!isMobileView || !isPanelView) && (
<View <View
style={[ style={[{ flex: 1, width: isMobileView ? '100%' : '700px' }, contentStyle]}
{ flex: 1, width: isMobileView ? "100%" : "700px" },
contentStyle,
]}
> >
{isMobileView && ( {isMobileView && (
<Button <Button
style={{ style={{
position: "absolute", position: 'absolute',
top: "10px", top: '10px',
left: "10px", left: '10px',
zIndex: 100, zIndex: 100,
}} }}
onPress={() => setIsPanelView(true)} onPress={() => setIsPanelView(true)}
@@ -223,4 +213,4 @@ export default createNavigatorFactory<
TabNavigationOptions, TabNavigationOptions,
TabNavigationEventMap, TabNavigationEventMap,
typeof TabNavigator typeof TabNavigator
>(TabNavigator); >(TabNavigator);

View File

@@ -1,15 +1,15 @@
import API from "../../API"; import API from '../../API';
import Song, { SongWithArtist } from "../../models/Song"; import { SongWithArtist } from '../../models/Song';
export const getSongWArtistSuggestions = async () => { export const getSongWArtistSuggestions = async () => {
const nextStepQuery = await API.getSongSuggestions(); const nextStepQuery = await API.getSongSuggestions();
const songWartist = await Promise.all( const songWartist = await Promise.all(
nextStepQuery.map(async (song) => { nextStepQuery.map(async (song) => {
if (!song.artistId) throw new Error("Song has no artistId"); if (!song.artistId) throw new Error('Song has no artistId');
const artist = await API.getArtist(song.artistId); const artist = await API.getArtist(song.artistId);
return { ...song, artist } as SongWithArtist; return { ...song, artist } as SongWithArtist;
}) })
); );
return songWartist; return songWartist;
}; };

View File

@@ -17,4 +17,4 @@
} }
} }
} }
} }

View File

@@ -1,5 +1,5 @@
import { Appearance } from "react-native"; import { Appearance } from 'react-native';
import { useSelector } from "../state/Store"; import { useSelector } from '../state/Store';
const useColorScheme = (): 'light' | 'dark' => { const useColorScheme = (): 'light' | 'dark' => {
const colorScheme = useSelector((state) => state.settings.local.colorScheme); const colorScheme = useSelector((state) => state.settings.local.colorScheme);
@@ -9,6 +9,6 @@ const useColorScheme = (): 'light' | 'dark' => {
return systemColorScheme ?? 'light'; return systemColorScheme ?? 'light';
} }
return colorScheme; return colorScheme;
} };
export default useColorScheme; export default useColorScheme;

View File

@@ -1,13 +1,12 @@
import { useQuery } from "react-query" import { useQuery } from 'react-query';
import API from "../API" import API from '../API';
const useUserSettings = () => { const useUserSettings = () => {
const queryKey = ['settings']; const queryKey = ['settings'];
const settings = useQuery(queryKey, () => API.getUserSettings()) const settings = useQuery(queryKey, () => API.getUserSettings());
const updateSettings = (...params: Parameters<typeof API.updateUserSettings>) => API const updateSettings = (...params: Parameters<typeof API.updateUserSettings>) =>
.updateUserSettings(...params) API.updateUserSettings(...params).then(() => settings.refetch());
.then(() => settings.refetch()); return { settings, updateSettings };
return { settings, updateSettings } };
}
export default useUserSettings; export default useUserSettings;

View File

@@ -1,9 +1,9 @@
import { RootState, useSelector } from "../state/Store"; import { RootState, useSelector } from '../state/Store';
import i18n from "./i18n"; import i18n from './i18n';
type LanguageGateProps = { type LanguageGateProps = {
children: any; children: JSX.Element;
} };
/** /**
* Gate to handle language update at startup and on every dispatch * Gate to handle language update at startup and on every dispatch
@@ -13,6 +13,6 @@ const LanguageGate = (props: LanguageGateProps) => {
const language = useSelector((state: RootState) => state.language.value); const language = useSelector((state: RootState) => state.language.value);
i18n.changeLanguage(language); i18n.changeLanguage(language);
return props.children; return props.children;
} };
export default LanguageGate; export default LanguageGate;

View File

@@ -1,540 +1,539 @@
export const en = { export const en = {
welcome: "Welcome", welcome: 'Welcome',
welcomeMessage: "Welcome back ", welcomeMessage: 'Welcome back ',
signOutBtn: "Sign out", signOutBtn: 'Sign out',
signInBtn: "Sign in", signInBtn: 'Sign in',
signUpBtn: "Sign up", signUpBtn: 'Sign up',
changeLanguageBtn: "Change language", changeLanguageBtn: 'Change language',
search: "Search", search: 'Search',
login: "Login", login: 'Login',
signUp: "Sign up", signUp: 'Sign up',
signIn: "Sign in", signIn: 'Sign in',
searchBtn: "Search", searchBtn: 'Search',
play: "Play", play: 'Play',
playBtn: "Play", playBtn: 'Play',
practiceBtn: "Practice", practiceBtn: 'Practice',
playAgain: "Play Again", playAgain: 'Play Again',
songPageBtn: "Go to song page", songPageBtn: 'Go to song page',
level: "Level", level: 'Level',
chapters: "Chapters", chapters: 'Chapters',
bestScore: "Best Score", bestScore: 'Best Score',
lastScore: "Last Score", lastScore: 'Last Score',
langBtn: "Language", langBtn: 'Language',
backBtn: "Back", backBtn: 'Back',
settingsBtn: "Settings", settingsBtn: 'Settings',
prefBtn: "Preferences", prefBtn: 'Preferences',
notifBtn: "Notifications", notifBtn: 'Notifications',
privBtn: "Privacy", privBtn: 'Privacy',
goNextStep: "Step Up!", goNextStep: 'Step Up!',
mySkillsToImprove: "My Competencies to work on", mySkillsToImprove: 'My Competencies to work on',
recentlyPlayed: "Recently played", recentlyPlayed: 'Recently played',
songsToGetBetter: "Recommendations", songsToGetBetter: 'Recommendations',
lastSearched: "Last searched", lastSearched: 'Last searched',
levelProgress: "good notes", levelProgress: 'good notes',
score: "Score", score: 'Score',
//search //search
allFilter: "All", allFilter: 'All',
artistFilter: "Artists", artistFilter: 'Artists',
songsFilter: "Songs", songsFilter: 'Songs',
genreFilter: "Genres", genreFilter: 'Genres',
// profile page // profile page
user: "Profile", user: 'Profile',
medals: "Medals", medals: 'Medals',
playerStats: "My stats", playerStats: 'My stats',
mostPlayedSong: "Most played song : ", mostPlayedSong: 'Most played song : ',
goodNotesPlayed: "Good notes played : ", goodNotesPlayed: 'Good notes played : ',
longestCombo: "Longest combo : ", longestCombo: 'Longest combo : ',
favoriteGenre: "Favorite genre : ", favoriteGenre: 'Favorite genre : ',
// Difficulty settings // Difficulty settings
diffBtn: "Difficulty", diffBtn: 'Difficulty',
easy: "Beginner", easy: 'Beginner',
medium: "Intermediate", medium: 'Intermediate',
hard: "Pro", hard: 'Pro',
// theme settings // theme settings
dark: "Dark", dark: 'Dark',
system: "System", system: 'System',
light: "Light", light: 'Light',
// competencies // competencies
pedalsCompetency: "Pedals", pedalsCompetency: 'Pedals',
rightHandCompetency: "Right hand", rightHandCompetency: 'Right hand',
leftHandCompetency: "Left hand", leftHandCompetency: 'Left hand',
accuracyCompetency: "Accuracy", accuracyCompetency: 'Accuracy',
arpegeCompetency: "Arpeges", arpegeCompetency: 'Arpeges',
chordsCompetency: "Chords", chordsCompetency: 'Chords',
/* account settings and logs */ /* account settings and logs */
// buttons // buttons
requirementsSentence: requirementsSentence:
"Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character", 'Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character',
// feedback for the user // feedback for the user
usernameTooShort: "Username is too short", usernameTooShort: 'Username is too short',
passwordTooShort: "Password is too short", passwordTooShort: 'Password is too short',
usernameTooLong: "Username is too long", usernameTooLong: 'Username is too long',
passwordTooLong: "Password is too long", passwordTooLong: 'Password is too long',
passwordsDontMatch: "Passwords don't match", passwordsDontMatch: "Passwords don't match",
invalidCredentials: "Invalid credentials", invalidCredentials: 'Invalid credentials',
invalidEmail: "Invalid email", invalidEmail: 'Invalid email',
accountCreated: "Account created", accountCreated: 'Account created',
loggedIn: "Logged in", loggedIn: 'Logged in',
precisionScore: "Precision", precisionScore: 'Precision',
goodNotesInARow: "Good notes in a row", goodNotesInARow: 'Good notes in a row',
usernameTaken: "Username already taken", usernameTaken: 'Username already taken',
goodNotes: "good notes", goodNotes: 'good notes',
// categories // categories
username: "Username", username: 'Username',
password: "Password", password: 'Password',
email: "Email", email: 'Email',
repeatPassword: "Repeat password", repeatPassword: 'Repeat password',
changepasswdBtn: "Change Password", changepasswdBtn: 'Change Password',
changeemailBtn: "Change Email", changeemailBtn: 'Change Email',
googleacctBtn: "Google Account", googleacctBtn: 'Google Account',
forgottenPassword: "Forgotten password", forgottenPassword: 'Forgotten password',
partition: "Partition", partition: 'Partition',
//errors //errors
unknownError: "Unknown error", unknownError: 'Unknown error',
errAlrdExst: "Already exist", errAlrdExst: 'Already exist',
errIncrrct: "Incorrect Credentials", errIncrrct: 'Incorrect Credentials',
errNoResults: "No Results Found", errNoResults: 'No Results Found',
userProfileFetchError: "An error occured while fetching your profile", userProfileFetchError: 'An error occured while fetching your profile',
tryAgain: "Try Again", tryAgain: 'Try Again',
// Playback messages // Playback messages
missed: "Missed note", missed: 'Missed note',
perfect: "Perfect", perfect: 'Perfect',
great: "Great", great: 'Great',
good: "Good", good: 'Good',
bestStreak: "Best streak", bestStreak: 'Best streak',
precision: "Precision", precision: 'Precision',
wrong: "Wrong", wrong: 'Wrong',
short: "A little too short", short: 'A little too short',
long: "A little too long", long: 'A little too long',
tooLong: "Too Long", tooLong: 'Too Long',
tooShort: "Too Short", tooShort: 'Too Short',
changePassword: "Change password", changePassword: 'Change password',
oldPassword: "Old password", oldPassword: 'Old password',
newPassword: "New password", newPassword: 'New password',
confirmNewPassword: "Confirm new password", confirmNewPassword: 'Confirm new password',
submitBtn: "Submit", submitBtn: 'Submit',
changeEmail: "Change email", changeEmail: 'Change email',
oldEmail: "Old email", oldEmail: 'Old email',
newEmail: "New email", newEmail: 'New email',
passwordUpdated: "Password updated", passwordUpdated: 'Password updated',
emailUpdated: "Email updated", emailUpdated: 'Email updated',
SettingsCategoryProfile: "Profile", SettingsCategoryProfile: 'Profile',
SettingsCategoryPreferences: "Preferences", SettingsCategoryPreferences: 'Preferences',
SettingsCategoryNotifications: "Notifications", SettingsCategoryNotifications: 'Notifications',
SettingsCategoryPrivacy: "Privacy", SettingsCategoryPrivacy: 'Privacy',
SettingsCategorySecurity: "Security", SettingsCategorySecurity: 'Security',
SettingsCategoryEmail: "Email", SettingsCategoryEmail: 'Email',
SettingsCategoryGoogle: "Google", SettingsCategoryGoogle: 'Google',
SettingsCategoryPiano: "Piano", SettingsCategoryPiano: 'Piano',
SettingsCategoryGuest: "Guest", SettingsCategoryGuest: 'Guest',
transformGuestToUserExplanations: transformGuestToUserExplanations:
"You can transform your guest account to a user account by providing a username and a password. You will then be able to save your progress and access your profile.", 'You can transform your guest account to a user account by providing a username and a password. You will then be able to save your progress and access your profile.',
SettingsNotificationsPushNotifications: "Push", SettingsNotificationsPushNotifications: 'Push',
SettingsNotificationsEmailNotifications: "Email", SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsTrainingReminder: "Training reminder", SettingsNotificationsTrainingReminder: 'Training reminder',
SettingsNotificationsReleaseAlert: "Release alert", SettingsNotificationsReleaseAlert: 'Release alert',
dataCollection: "Data collection", dataCollection: 'Data collection',
customAds: "Custom ads", customAds: 'Custom ads',
recommendations: "Recommendations", recommendations: 'Recommendations',
SettingsPreferencesTheme: "Theme", SettingsPreferencesTheme: 'Theme',
SettingsPreferencesLanguage: "Language", SettingsPreferencesLanguage: 'Language',
SettingsPreferencesDifficulty: "Difficulty", SettingsPreferencesDifficulty: 'Difficulty',
SettingsPreferencesColorblindMode: "Colorblind mode", SettingsPreferencesColorblindMode: 'Colorblind mode',
SettingsPreferencesMicVolume: "Mic volume", SettingsPreferencesMicVolume: 'Mic volume',
SettingsPreferencesDevice: "Device", SettingsPreferencesDevice: 'Device',
NoAssociatedEmail: "No associated email", NoAssociatedEmail: 'No associated email',
nbGamesPlayed: "Games played", nbGamesPlayed: 'Games played',
XPDescription: XPDescription:
"XP is a measure of your progress. You earn XP by playing songs and completing challenges.", 'XP is a measure of your progress. You earn XP by playing songs and completing challenges.',
userCreatedAt: "Creation date", userCreatedAt: 'Creation date',
premiumAccount: "Premium account", premiumAccount: 'Premium account',
yes: "Yes", yes: 'Yes',
no: "No", no: 'No',
Attention: "Attention", Attention: 'Attention',
YouAreCurrentlyConnectedWithAGuestAccountWarning: YouAreCurrentlyConnectedWithAGuestAccountWarning:
"You are currently connected with a guest account. Disconneting will result in your data being lost. If you want to save your progress, you need to create an account.", 'You are currently connected with a guest account. Disconneting will result in your data being lost. If you want to save your progress, you need to create an account.',
recentSearches: "Recent searches", recentSearches: 'Recent searches',
noRecentSearches: "No recent searches", noRecentSearches: 'No recent searches',
}; };
export const fr: typeof en = { export const fr: typeof en = {
welcome: "Bienvenue", welcome: 'Bienvenue',
welcomeMessage: "Re-Bonjour ", welcomeMessage: 'Re-Bonjour ',
signOutBtn: "Se déconnecter", signOutBtn: 'Se déconnecter',
signInBtn: "Se connecter", signInBtn: 'Se connecter',
changeLanguageBtn: "Changer la langue", changeLanguageBtn: 'Changer la langue',
searchBtn: "Rechercher", searchBtn: 'Rechercher',
playBtn: "Jouer", playBtn: 'Jouer',
practiceBtn: "S'entrainer", practiceBtn: "S'entrainer",
playAgain: "Rejouer", playAgain: 'Rejouer',
songPageBtn: "Aller sur la page de la chanson", songPageBtn: 'Aller sur la page de la chanson',
level: "Niveau", level: 'Niveau',
chapters: "Chapitres", chapters: 'Chapitres',
bestScore: "Meilleur Score", bestScore: 'Meilleur Score',
lastScore: "Dernier Score", lastScore: 'Dernier Score',
bestStreak: "Meilleure série", bestStreak: 'Meilleure série',
precision: "Précision", precision: 'Précision',
langBtn: "Langage", langBtn: 'Langage',
backBtn: "Retour", backBtn: 'Retour',
prefBtn: "Préférences", prefBtn: 'Préférences',
notifBtn: "Notifications", notifBtn: 'Notifications',
privBtn: "Confidentialité", privBtn: 'Confidentialité',
play: "Jouer", play: 'Jouer',
changeEmail: "Changer d'email", changeEmail: "Changer d'email",
newEmail: "Nouvel email", newEmail: 'Nouvel email',
oldEmail: "Ancien email", oldEmail: 'Ancien email',
// profile page // profile page
user: "Profil", user: 'Profil',
medals: "Medailles", medals: 'Medailles',
playerStats: "Mes statistiques", playerStats: 'Mes statistiques',
mostPlayedSong: "Chanson la plus jouée : ", mostPlayedSong: 'Chanson la plus jouée : ',
goodNotesPlayed: "Bonnes notes jouées : ", goodNotesPlayed: 'Bonnes notes jouées : ',
longestCombo: "Combo le plus long : ", longestCombo: 'Combo le plus long : ',
favoriteGenre: "Genre favori : ", favoriteGenre: 'Genre favori : ',
//search //search
allFilter: "Tout", allFilter: 'Tout',
artistFilter: "Artistes", artistFilter: 'Artistes',
songsFilter: "Morceaux", songsFilter: 'Morceaux',
genreFilter: "Genres", genreFilter: 'Genres',
// Difficulty settings // Difficulty settings
diffBtn: "Difficulté", diffBtn: 'Difficulté',
easy: "Débutant", easy: 'Débutant',
medium: "Intermédiaire", medium: 'Intermédiaire',
hard: "Avancé", hard: 'Avancé',
// theme settings // theme settings
dark: "Foncé", dark: 'Foncé',
system: "Système", system: 'Système',
light: "Clair", light: 'Clair',
settingsBtn: "Réglages", settingsBtn: 'Réglages',
goNextStep: "Prochaine Etape", goNextStep: 'Prochaine Etape',
mySkillsToImprove: "Mes Skills", mySkillsToImprove: 'Mes Skills',
recentlyPlayed: "Joués récemment", recentlyPlayed: 'Joués récemment',
search: "Rechercher", search: 'Rechercher',
lastSearched: "Dernières recherches", lastSearched: 'Dernières recherches',
levelProgress: "Niveau", levelProgress: 'Niveau',
login: "Se connecter", login: 'Se connecter',
signUp: "S'inscrire", signUp: "S'inscrire",
signIn: "Se connecter", signIn: 'Se connecter',
oldPassword: "Ancien mot de passe", oldPassword: 'Ancien mot de passe',
newPassword: "Nouveau mot de passe", newPassword: 'Nouveau mot de passe',
confirmNewPassword: "Confirmer le nouveau mot de passe", confirmNewPassword: 'Confirmer le nouveau mot de passe',
submitBtn: "Soumettre", submitBtn: 'Soumettre',
// competencies // competencies
pedalsCompetency: "Pédales", pedalsCompetency: 'Pédales',
rightHandCompetency: "Main droite", rightHandCompetency: 'Main droite',
leftHandCompetency: "Main gauche", leftHandCompetency: 'Main gauche',
accuracyCompetency: "Justesse", accuracyCompetency: 'Justesse',
arpegeCompetency: "Arpege", arpegeCompetency: 'Arpege',
chordsCompetency: "Accords", chordsCompetency: 'Accords',
/* account settings and logs */ /* account settings and logs */
// buttons // buttons
requirementsSentence: requirementsSentence:
"Doit contenir 8 caractères, une majuscule, une minuscule, un chiffre et un caractère spécial", 'Doit contenir 8 caractères, une majuscule, une minuscule, un chiffre et un caractère spécial',
// feedback for the user // feedback for the user
usernameTooShort: "Le nom d'utilisateur est trop court", usernameTooShort: "Le nom d'utilisateur est trop court",
passwordTooShort: "Le mot de passe est trop court", passwordTooShort: 'Le mot de passe est trop court',
usernameTooLong: "Le nom d'utilisateur est trop long", usernameTooLong: "Le nom d'utilisateur est trop long",
passwordTooLong: "Le mot de passe est trop long", passwordTooLong: 'Le mot de passe est trop long',
passwordsDontMatch: "Mots de passes différents", passwordsDontMatch: 'Mots de passes différents',
invalidCredentials: "Identifiants incorrects", invalidCredentials: 'Identifiants incorrects',
invalidEmail: "Email invalide", invalidEmail: 'Email invalide',
loggedIn: "Connecté", loggedIn: 'Connecté',
usernameTaken: "Nom d'utilisateur déjà pris", usernameTaken: "Nom d'utilisateur déjà pris",
accountCreated: "Compte créé", accountCreated: 'Compte créé',
// categories // categories
username: "Nom d'utilisateur", username: "Nom d'utilisateur",
password: "Mot de passe", password: 'Mot de passe',
email: "Email", email: 'Email',
repeatPassword: "Confirmer", repeatPassword: 'Confirmer',
score: "Score", score: 'Score',
changePassword: "Modification du mot de passe", changePassword: 'Modification du mot de passe',
precisionScore: "Précision", precisionScore: 'Précision',
goodNotesInARow: "Bonnes notes à la suite", goodNotesInARow: 'Bonnes notes à la suite',
songsToGetBetter: "Recommendations", songsToGetBetter: 'Recommendations',
goodNotes: "bonnes notes", goodNotes: 'bonnes notes',
changepasswdBtn: "Changer le mot de passe", changepasswdBtn: 'Changer le mot de passe',
changeemailBtn: "Changer l'email", changeemailBtn: "Changer l'email",
googleacctBtn: "Compte Google", googleacctBtn: 'Compte Google',
forgottenPassword: "Mot de passe oublié", forgottenPassword: 'Mot de passe oublié',
partition: "Partition", partition: 'Partition',
signUpBtn: "S'inscrire", signUpBtn: "S'inscrire",
//errors //errors
errAlrdExst: "Utilisateur existe déjà", errAlrdExst: 'Utilisateur existe déjà',
unknownError: "Erreur inconnue", unknownError: 'Erreur inconnue',
errIncrrct: "Identifiant incorrect", errIncrrct: 'Identifiant incorrect',
errNoResults: "Aucun resultat", errNoResults: 'Aucun resultat',
userProfileFetchError: userProfileFetchError: 'Une erreur est survenue lors de la récupération du profil',
"Une erreur est survenue lors de la récupération du profil", tryAgain: 'Réessayer',
tryAgain: "Réessayer",
// Playback messages // Playback messages
missed: "Raté", missed: 'Raté',
perfect: "Parfait", perfect: 'Parfait',
great: "Super", great: 'Super',
good: "Bien", good: 'Bien',
wrong: "Oups", wrong: 'Oups',
short: "Un peu court", short: 'Un peu court',
long: "Un peu long", long: 'Un peu long',
tooLong: "Trop long", tooLong: 'Trop long',
tooShort: "Trop court", tooShort: 'Trop court',
passwordUpdated: "Mot de passe mis à jour", passwordUpdated: 'Mot de passe mis à jour',
emailUpdated: "Email mis à jour", emailUpdated: 'Email mis à jour',
SettingsCategoryProfile: "Profil", SettingsCategoryProfile: 'Profil',
SettingsCategoryPreferences: "Préférences", SettingsCategoryPreferences: 'Préférences',
SettingsCategoryNotifications: "Notifications", SettingsCategoryNotifications: 'Notifications',
SettingsCategoryPrivacy: "Confidentialité", SettingsCategoryPrivacy: 'Confidentialité',
SettingsCategorySecurity: "Sécurité", SettingsCategorySecurity: 'Sécurité',
SettingsCategoryEmail: "Email", SettingsCategoryEmail: 'Email',
SettingsCategoryGoogle: "Google", SettingsCategoryGoogle: 'Google',
SettingsCategoryPiano: "Piano", SettingsCategoryPiano: 'Piano',
transformGuestToUserExplanations: transformGuestToUserExplanations:
"Vous êtes actuellement connecté en tant qu'invité. Vous pouvez créer un compte pour sauvegarder vos données et profiter de toutes les fonctionnalités de Chromacase.", "Vous êtes actuellement connecté en tant qu'invité. Vous pouvez créer un compte pour sauvegarder vos données et profiter de toutes les fonctionnalités de Chromacase.",
SettingsCategoryGuest: "Invité", SettingsCategoryGuest: 'Invité',
SettingsNotificationsEmailNotifications: "Email", SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsPushNotifications: "Notifications push", SettingsNotificationsPushNotifications: 'Notifications push',
SettingsNotificationsReleaseAlert: "Alertes de nouvelles Sorties", SettingsNotificationsReleaseAlert: 'Alertes de nouvelles Sorties',
SettingsNotificationsTrainingReminder: "Rappel d'entrainement", SettingsNotificationsTrainingReminder: "Rappel d'entrainement",
SettingsPreferencesColorblindMode: "Mode daltonien", SettingsPreferencesColorblindMode: 'Mode daltonien',
SettingsPreferencesDevice: "Appareil", SettingsPreferencesDevice: 'Appareil',
SettingsPreferencesDifficulty: "Difficulté", SettingsPreferencesDifficulty: 'Difficulté',
SettingsPreferencesLanguage: "Langue", SettingsPreferencesLanguage: 'Langue',
SettingsPreferencesTheme: "Thème", SettingsPreferencesTheme: 'Thème',
SettingsPreferencesMicVolume: "Volume du micro", SettingsPreferencesMicVolume: 'Volume du micro',
dataCollection: "Collecte de données", dataCollection: 'Collecte de données',
recommendations: "Recommandations", recommendations: 'Recommandations',
customAds: "Publicités personnalisées", customAds: 'Publicités personnalisées',
NoAssociatedEmail: "Aucun email associé", NoAssociatedEmail: 'Aucun email associé',
nbGamesPlayed: "Parties jouées", nbGamesPlayed: 'Parties jouées',
XPDescription: XPDescription:
"L'XP est gagnée en jouant des chansons. Plus vous jouez, plus vous gagnez d'XP. Plus vous avez d'XP, plus vous montez de niveau.", "L'XP est gagnée en jouant des chansons. Plus vous jouez, plus vous gagnez d'XP. Plus vous avez d'XP, plus vous montez de niveau.",
userCreatedAt: "Compte créé le", userCreatedAt: 'Compte créé le',
premiumAccount: "Compte premium", premiumAccount: 'Compte premium',
yes: "Oui", yes: 'Oui',
no: "Non", no: 'Non',
Attention: "Attention", Attention: 'Attention',
YouAreCurrentlyConnectedWithAGuestAccountWarning: YouAreCurrentlyConnectedWithAGuestAccountWarning:
"Vous êtes actuellement connecté en tant qu'invité. La déconnexion résultera en une perte de données. Vous pouvez créer un compte pour sauvegarder vos données.", "Vous êtes actuellement connecté en tant qu'invité. La déconnexion résultera en une perte de données. Vous pouvez créer un compte pour sauvegarder vos données.",
recentSearches: "Recherches récentes", recentSearches: 'Recherches récentes',
noRecentSearches: "Aucune recherche récente", noRecentSearches: 'Aucune recherche récente',
}; };
export const sp: typeof en = { export const sp: typeof en = {
welcomeMessage: "Benvenido", welcomeMessage: 'Benvenido',
signOutBtn: "Desconectarse", signOutBtn: 'Desconectarse',
signInBtn: "Connectarse", signInBtn: 'Connectarse',
changepasswdBtn: "Cambiar la contraseña", changepasswdBtn: 'Cambiar la contraseña',
changeemailBtn: "Cambiar e-mail", changeemailBtn: 'Cambiar e-mail',
googleacctBtn: "Cuenta Google", googleacctBtn: 'Cuenta Google',
goodNotes: "buenas notas", goodNotes: 'buenas notas',
search: "Buscar", search: 'Buscar',
login: "Iniciar sesión", login: 'Iniciar sesión',
signUp: "Registrarse", signUp: 'Registrarse',
signIn: "Iniciar sesión", signIn: 'Iniciar sesión',
changeEmail: "Cambiar el correo electrónico", changeEmail: 'Cambiar el correo electrónico',
newEmail: "Nuevo correo electrónico", newEmail: 'Nuevo correo electrónico',
oldEmail: "Correo electrónico anterior", oldEmail: 'Correo electrónico anterior',
// competencies // competencies
changeLanguageBtn: "Cambiar el idioma", changeLanguageBtn: 'Cambiar el idioma',
searchBtn: "Buscar", searchBtn: 'Buscar',
playBtn: "reproducir", playBtn: 'reproducir',
practiceBtn: "Práctica", practiceBtn: 'Práctica',
playAgain: "Repetición", playAgain: 'Repetición',
precisionScore: "Précision", precisionScore: 'Précision',
songPageBtn: "canción", songPageBtn: 'canción',
level: "nivele", level: 'nivele',
chapters: "Capítulos", chapters: 'Capítulos',
bestScore: "Mejor puntuación", bestScore: 'Mejor puntuación',
lastScore: "Ùltima puntuación", lastScore: 'Ùltima puntuación',
prefBtn: "Preferencias", prefBtn: 'Preferencias',
notifBtn: "Notificaciones", notifBtn: 'Notificaciones',
privBtn: "Privacidad", privBtn: 'Privacidad',
goNextStep: "Da el siguiente paso", goNextStep: 'Da el siguiente paso',
mySkillsToImprove: "Mis habilidades para mejorar", mySkillsToImprove: 'Mis habilidades para mejorar',
recentlyPlayed: "Recientemente jugado", recentlyPlayed: 'Recientemente jugado',
lastSearched: "Ultimas búsquedas", lastSearched: 'Ultimas búsquedas',
welcome: "Benvenido a Chromacase", welcome: 'Benvenido a Chromacase',
langBtn: "Langua", langBtn: 'Langua',
backBtn: "Volver", backBtn: 'Volver',
settingsBtn: "Ajustes", settingsBtn: 'Ajustes',
play: "Jugar", play: 'Jugar',
// profile page // profile page
user: "Perfil", user: 'Perfil',
medals: "Medallas", medals: 'Medallas',
playerStats: "mis estadísticas", playerStats: 'mis estadísticas',
mostPlayedSong: "Canción más reproducida : ", mostPlayedSong: 'Canción más reproducida : ',
goodNotesPlayed: "Buenas notas tocadas : ", goodNotesPlayed: 'Buenas notas tocadas : ',
longestCombo: "combo más largo : ", longestCombo: 'combo más largo : ',
favoriteGenre: "genero favorito : ", favoriteGenre: 'genero favorito : ',
//search //search
allFilter: "Todos", allFilter: 'Todos',
artistFilter: "Artistas", artistFilter: 'Artistas',
songsFilter: "canciones", songsFilter: 'canciones',
genreFilter: "géneros", genreFilter: 'géneros',
// Difficulty settings // Difficulty settings
diffBtn: "Dificultad", diffBtn: 'Dificultad',
easy: "Principiante", easy: 'Principiante',
medium: "Intermedio", medium: 'Intermedio',
hard: "Avanzado", hard: 'Avanzado',
// theme settings // theme settings
dark: "Oscuro", dark: 'Oscuro',
system: "Sistema", system: 'Sistema',
light: "Claro", light: 'Claro',
// competencies // competencies
pedalsCompetency: "Pedal", pedalsCompetency: 'Pedal',
rightHandCompetency: "Mano derecha", rightHandCompetency: 'Mano derecha',
leftHandCompetency: "Mano izquierda", leftHandCompetency: 'Mano izquierda',
accuracyCompetency: "Exactitud", accuracyCompetency: 'Exactitud',
arpegeCompetency: "Arpegios", arpegeCompetency: 'Arpegios',
chordsCompetency: "Accords", chordsCompetency: 'Accords',
/* account settings and logs */ /* account settings and logs */
// buttons // buttons
requirementsSentence: requirementsSentence:
"Debe contener 8 caracteres, una mayúscula, una minúscula, un número y un carácter especial", 'Debe contener 8 caracteres, una mayúscula, una minúscula, un número y un carácter especial',
// feedback for the user // feedback for the user
usernameTooShort: "Nombre de usuario demasiado corto", usernameTooShort: 'Nombre de usuario demasiado corto',
passwordTooShort: "Contraseña demasiado corta", passwordTooShort: 'Contraseña demasiado corta',
usernameTooLong: "Nombre de usuario demasiado largo", usernameTooLong: 'Nombre de usuario demasiado largo',
passwordTooLong: "Contraseña demasiado larga", passwordTooLong: 'Contraseña demasiado larga',
passwordsDontMatch: "Las contraseñas no coinciden", passwordsDontMatch: 'Las contraseñas no coinciden',
invalidCredentials: "Credenciales incorrectas", invalidCredentials: 'Credenciales incorrectas',
forgottenPassword: "Contraseña olvidada", forgottenPassword: 'Contraseña olvidada',
invalidEmail: "Email inválido", invalidEmail: 'Email inválido',
accountCreated: "Cuenta creada", accountCreated: 'Cuenta creada',
loggedIn: "Connectado", loggedIn: 'Connectado',
usernameTaken: "Nombre de usuario ya tomado", usernameTaken: 'Nombre de usuario ya tomado',
score: "Puntaje", score: 'Puntaje',
goodNotesInARow: "Buenas notas despues", goodNotesInARow: 'Buenas notas despues',
songsToGetBetter: "Recomendaciones", songsToGetBetter: 'Recomendaciones',
// categories // categories
username: "Nombre del usuario", username: 'Nombre del usuario',
password: "Contraseña", password: 'Contraseña',
email: "Correo electrónico", email: 'Correo electrónico',
repeatPassword: "Repita la contraseña", repeatPassword: 'Repita la contraseña',
partition: "Partitura", partition: 'Partitura',
levelProgress: "Nivel progreso", levelProgress: 'Nivel progreso',
signUpBtn: "Inscribirse", signUpBtn: 'Inscribirse',
//errors //errors
unknownError: "Error desconocido", unknownError: 'Error desconocido',
errAlrdExst: "Ya existe", errAlrdExst: 'Ya existe',
errIncrrct: "credenciales incorrectas", errIncrrct: 'credenciales incorrectas',
errNoResults: "No se han encontrado resultados", errNoResults: 'No se han encontrado resultados',
userProfileFetchError: "Ocurrió un error al obtener su perfil", userProfileFetchError: 'Ocurrió un error al obtener su perfil',
tryAgain: "intentar otra vez", tryAgain: 'intentar otra vez',
// Playback messages // Playback messages
missed: "Te perdiste una nota", missed: 'Te perdiste una nota',
perfect: "Perfecto", perfect: 'Perfecto',
great: "Excelente", great: 'Excelente',
good: "Bueno", good: 'Bueno',
bestStreak: "Mejor racha", bestStreak: 'Mejor racha',
precision: "Precisión", precision: 'Precisión',
wrong: "Equivocado", wrong: 'Equivocado',
short: "Un poco demasiado corto", short: 'Un poco demasiado corto',
long: "Un poco demasiado largo", long: 'Un poco demasiado largo',
tooLong: "Demasiado largo", tooLong: 'Demasiado largo',
tooShort: "Demasiado corto", tooShort: 'Demasiado corto',
changePassword: "Cambio de contraseña", changePassword: 'Cambio de contraseña',
oldPassword: "Contraseña anterior", oldPassword: 'Contraseña anterior',
newPassword: "Nueva contraseña", newPassword: 'Nueva contraseña',
confirmNewPassword: "Confirmar nueva contraseña", confirmNewPassword: 'Confirmar nueva contraseña',
submitBtn: "Enviar", submitBtn: 'Enviar',
passwordUpdated: "Contraseña actualizada", passwordUpdated: 'Contraseña actualizada',
emailUpdated: "Email actualizado", emailUpdated: 'Email actualizado',
SettingsCategoryProfile: "Perfil", SettingsCategoryProfile: 'Perfil',
SettingsCategoryPreferences: "Preferencias", SettingsCategoryPreferences: 'Preferencias',
SettingsCategoryNotifications: "Notificaciones", SettingsCategoryNotifications: 'Notificaciones',
SettingsCategoryPrivacy: "Privacidad", SettingsCategoryPrivacy: 'Privacidad',
SettingsCategorySecurity: "Seguridad", SettingsCategorySecurity: 'Seguridad',
SettingsCategoryEmail: "Email", SettingsCategoryEmail: 'Email',
SettingsCategoryGoogle: "Google", SettingsCategoryGoogle: 'Google',
SettingsCategoryPiano: "Piano", SettingsCategoryPiano: 'Piano',
transformGuestToUserExplanations: transformGuestToUserExplanations:
"Actualmente estás conectado como invitado. Puedes crear una cuenta para guardar tus datos y disfrutar de todas las funciones de Chromacase.", 'Actualmente estás conectado como invitado. Puedes crear una cuenta para guardar tus datos y disfrutar de todas las funciones de Chromacase.',
SettingsCategoryGuest: "Invitado", SettingsCategoryGuest: 'Invitado',
SettingsNotificationsEmailNotifications: "Email", SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsPushNotifications: "Notificaciones push", SettingsNotificationsPushNotifications: 'Notificaciones push',
SettingsNotificationsReleaseAlert: "Alertas de nuevas Sorties", SettingsNotificationsReleaseAlert: 'Alertas de nuevas Sorties',
SettingsNotificationsTrainingReminder: "Recordatorio de entrenamiento", SettingsNotificationsTrainingReminder: 'Recordatorio de entrenamiento',
SettingsPreferencesColorblindMode: "Modo daltoniano", SettingsPreferencesColorblindMode: 'Modo daltoniano',
SettingsPreferencesDevice: "Dispositivo", SettingsPreferencesDevice: 'Dispositivo',
SettingsPreferencesDifficulty: "Dificultad", SettingsPreferencesDifficulty: 'Dificultad',
SettingsPreferencesLanguage: "Idioma", SettingsPreferencesLanguage: 'Idioma',
SettingsPreferencesTheme: "Tema", SettingsPreferencesTheme: 'Tema',
SettingsPreferencesMicVolume: "Volumen del micrófono", SettingsPreferencesMicVolume: 'Volumen del micrófono',
dataCollection: "Recopilación de datos", dataCollection: 'Recopilación de datos',
recommendations: "Recomendaciones", recommendations: 'Recomendaciones',
customAds: "Anuncios personalizados", customAds: 'Anuncios personalizados',
NoAssociatedEmail: "No hay correo electrónico asociado", NoAssociatedEmail: 'No hay correo electrónico asociado',
nbGamesPlayed: "Partidos jugados", nbGamesPlayed: 'Partidos jugados',
XPDescription: XPDescription:
"XP se gana jugando canciones. Cuanto más juegas, más XP ganas. Cuanto más XP tienes, más subes de nivel.", 'XP se gana jugando canciones. Cuanto más juegas, más XP ganas. Cuanto más XP tienes, más subes de nivel.',
userCreatedAt: "Cuenta creada el", userCreatedAt: 'Cuenta creada el',
premiumAccount: "Cuenta premium", premiumAccount: 'Cuenta premium',
yes: "", yes: '',
no: "No", no: 'No',
Attention: "Atención", Attention: 'Atención',
YouAreCurrentlyConnectedWithAGuestAccountWarning: YouAreCurrentlyConnectedWithAGuestAccountWarning:
"Actualmente estás conectado como invitado. La desconexión resultará en la pérdida de datos. Puedes crear una cuenta para guardar tus datos.", 'Actualmente estás conectado como invitado. La desconexión resultará en la pérdida de datos. Puedes crear una cuenta para guardar tus datos.',
recentSearches: "Búsquedas recientes", recentSearches: 'Búsquedas recientes',
noRecentSearches: "No hay búsquedas recientes", noRecentSearches: 'No hay búsquedas recientes',
}; };

View File

@@ -1,32 +1,30 @@
import { en, fr, sp } from './Translations'; import { en, fr, sp } from './Translations';
import i18n from "i18next"; import i18n from 'i18next';
import { initReactI18next } from "react-i18next"; import { initReactI18next } from 'react-i18next';
import Translate from '../components/Translate'; import Translate from '../components/Translate';
export type AvailableLanguages = 'en' | 'fr' | 'sp'; export type AvailableLanguages = 'en' | 'fr' | 'sp';
export const DefaultLanguage: AvailableLanguages = 'en'; export const DefaultLanguage: AvailableLanguages = 'en';
i18n i18n.use(initReactI18next).init({
.use(initReactI18next) compatibilityJSON: 'v3',
.init({ resources: {
compatibilityJSON: 'v3', en: {
resources: { translation: en,
en: {
translation: en
},
fr: {
translation: fr
},
sp: {
translation: sp
}
}, },
lng: DefaultLanguage, fr: {
fallbackLng: 'en', translation: fr,
interpolation: { },
escapeValue: false sp: {
} translation: sp,
}); },
},
lng: DefaultLanguage,
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
});
export default i18n; export default i18n;
@@ -37,8 +35,8 @@ export default i18n;
*/ */
export const translate = (key: keyof typeof en, language?: AvailableLanguages) => { export const translate = (key: keyof typeof en, language?: AvailableLanguages) => {
return i18n.t(key, { return i18n.t(key, {
lng: language lng: language,
}); });
} };
export { Translate }; export { Translate };

View File

@@ -1,7 +1,7 @@
import Model from "./Model"; import Model from './Model';
interface Album extends Model { interface Album extends Model {
name: string; name: string;
} }
export default Album; export default Album;

View File

@@ -1,8 +1,8 @@
import Model from "./Model"; import Model from './Model';
interface Artist extends Model { interface Artist extends Model {
name: string; name: string;
picture?: string; picture?: string;
} }
export default Artist; export default Artist;

View File

@@ -1,3 +1,3 @@
type AuthToken = string; type AuthToken = string;
export default AuthToken; export default AuthToken;

View File

@@ -1,5 +1,5 @@
import Skill from "./Skill"; import Skill from './Skill';
import Model from "./Model"; import Model from './Model';
interface Chapter extends Model { interface Chapter extends Model {
start: number; start: number;
@@ -8,7 +8,7 @@ interface Chapter extends Model {
name: string; name: string;
type: 'chorus' | 'verse'; type: 'chorus' | 'verse';
key_aspect: Skill; key_aspect: Skill;
difficulty: number difficulty: number;
} }
export default Chapter; export default Chapter;

View File

@@ -1,7 +1,7 @@
import Model from "./Model"; import Model from './Model';
interface Genre extends Model { interface Genre extends Model {
name: string; name: string;
} }
export default Genre; export default Genre;

View File

@@ -1,5 +1,5 @@
import Skill from "./Skill"; import Skill from './Skill';
import Model from "./Model"; import Model from './Model';
/** /**
* A Lesson is an exercice that the user can try to practice a skill * A Lesson is an exercice that the user can try to practice a skill
@@ -8,7 +8,7 @@ interface Lesson extends Model {
/** /**
* The title of the lesson * The title of the lesson
*/ */
title: string, title: string;
/** /**
* Short description of the lesson * Short description of the lesson
*/ */
@@ -23,4 +23,4 @@ interface Lesson extends Model {
mainSkill: Skill; mainSkill: Skill;
} }
export default Lesson; export default Lesson;

View File

@@ -1,6 +1,6 @@
interface LessonHistory { interface LessonHistory {
lessonId: number; lessonId: number;
userId: number userId: number;
} }
export default LessonHistory; export default LessonHistory;

View File

@@ -1,10 +1,10 @@
export default interface LocalSettings { export default interface LocalSettings {
deviceId: number, deviceId: number;
micVolume: number, micVolume: number;
colorScheme: 'light' | 'dark' | 'system', colorScheme: 'light' | 'dark' | 'system';
lang: 'fr' | 'en' | 'sp', lang: 'fr' | 'en' | 'sp';
difficulty: 'beg' | 'inter' | 'pro', difficulty: 'beg' | 'inter' | 'pro';
colorBlind: boolean, colorBlind: boolean;
customAds: boolean, customAds: boolean;
dataCollection: boolean dataCollection: boolean;
} }

View File

@@ -2,4 +2,4 @@ interface Model {
id: number; id: number;
} }
export default Model; export default Model;

View File

@@ -1,28 +1,28 @@
export enum Note { export enum Note {
"C", 'C',
"C#", 'C#',
"D", 'D',
"D#", 'D#',
"E", 'E',
"F", 'F',
"F#", 'F#',
"G", 'G',
"G#", 'G#',
"A", 'A',
"A#", 'A#',
"B", 'B',
} }
export enum NoteNameBehavior { export enum NoteNameBehavior {
"always", 'always',
"onpress", 'onpress',
"onhighlight", 'onhighlight',
"onhover", 'onhover',
"never", 'never',
} }
export enum KeyPressStyle { export enum KeyPressStyle {
"subtle", 'subtle',
"vivid", 'vivid',
} }
export type HighlightedKey = { export type HighlightedKey = {
key: PianoKey; key: PianoKey;
@@ -40,54 +40,54 @@ export class PianoKey {
} }
public toString = () => { public toString = () => {
return (this.note as unknown as string) + (this.octave || ""); return (this.note as unknown as string) + (this.octave || '');
}; };
} }
export const strToKey = (str: string): PianoKey => { export const strToKey = (str: string): PianoKey => {
let note: Note; let note: Note;
const isSimpleNote = str[1]! >= "0" && str[1]! <= "9"; const isSimpleNote = str[1]! >= '0' && str[1]! <= '9';
// later we need to support different annotations // later we need to support different annotations
switch (isSimpleNote ? str[0] : str.substring(0, 2)) { switch (isSimpleNote ? str[0] : str.substring(0, 2)) {
case "E": case 'E':
note = Note.E; note = Note.E;
break; break;
case "B": case 'B':
note = Note.B; note = Note.B;
break; break;
case "C": case 'C':
note = Note.C; note = Note.C;
break; break;
case "D": case 'D':
note = Note.D; note = Note.D;
break; break;
case "F": case 'F':
note = Note.F; note = Note.F;
break; break;
case "G": case 'G':
note = Note.G; note = Note.G;
break; break;
case "A": case 'A':
note = Note.A; note = Note.A;
break; break;
case "C#": case 'C#':
note = Note["C#"]; note = Note['C#'];
break; break;
case "D#": case 'D#':
note = Note["D#"]; note = Note['D#'];
break; break;
case "F#": case 'F#':
note = Note["F#"]; note = Note['F#'];
break; break;
case "G#": case 'G#':
note = Note["G#"]; note = Note['G#'];
break; break;
case "A#": case 'A#':
note = Note["A#"]; note = Note['A#'];
break; break;
default: default:
throw new Error("Invalid note name"); throw new Error('Invalid note name');
} }
if ((isSimpleNote && !str[1]) || (!isSimpleNote && str.length < 3)) { if ((isSimpleNote && !str[1]) || (!isSimpleNote && str.length < 3)) {
return new PianoKey(note); return new PianoKey(note);
@@ -96,47 +96,47 @@ export const strToKey = (str: string): PianoKey => {
return new PianoKey(note, octave); return new PianoKey(note, octave);
}; };
export const keyToStr = (key: PianoKey, showOctave: boolean = true): string => { export const keyToStr = (key: PianoKey, showOctave = true): string => {
let s = ""; let s = '';
switch (key.note) { switch (key.note) {
case Note.C: case Note.C:
s += "C"; s += 'C';
break; break;
case Note.D: case Note.D:
s += "D"; s += 'D';
break; break;
case Note.E: case Note.E:
s += "E"; s += 'E';
break; break;
case Note.F: case Note.F:
s += "F"; s += 'F';
break; break;
case Note.G: case Note.G:
s += "G"; s += 'G';
break; break;
case Note.A: case Note.A:
s += "A"; s += 'A';
break; break;
case Note.B: case Note.B:
s += "B"; s += 'B';
break; break;
case Note["C#"]: case Note['C#']:
s += "C#"; s += 'C#';
break; break;
case Note["D#"]: case Note['D#']:
s += "D#"; s += 'D#';
break; break;
case Note["F#"]: case Note['F#']:
s += "F#"; s += 'F#';
break; break;
case Note["G#"]: case Note['G#']:
s += "G#"; s += 'G#';
break; break;
case Note["A#"]: case Note['A#']:
s += "A#"; s += 'A#';
break; break;
default: default:
throw new Error("Invalid note name"); throw new Error('Invalid note name');
} }
if (showOctave && key.octave) { if (showOctave && key.octave) {
s += key.octave; s += key.octave;
@@ -146,25 +146,25 @@ export const keyToStr = (key: PianoKey, showOctave: boolean = true): string => {
export const isAccidental = (key: PianoKey): boolean => { export const isAccidental = (key: PianoKey): boolean => {
return ( return (
key.note === Note["C#"] || key.note === Note['C#'] ||
key.note === Note["D#"] || key.note === Note['D#'] ||
key.note === Note["F#"] || key.note === Note['F#'] ||
key.note === Note["G#"] || key.note === Note['G#'] ||
key.note === Note["A#"] key.note === Note['A#']
); );
}; };
export const octaveKeys: Array<PianoKey> = [ export const octaveKeys: Array<PianoKey> = [
new PianoKey(Note.C), new PianoKey(Note.C),
new PianoKey(Note["C#"]), new PianoKey(Note['C#']),
new PianoKey(Note.D), new PianoKey(Note.D),
new PianoKey(Note["D#"]), new PianoKey(Note['D#']),
new PianoKey(Note.E), new PianoKey(Note.E),
new PianoKey(Note.F), new PianoKey(Note.F),
new PianoKey(Note["F#"]), new PianoKey(Note['F#']),
new PianoKey(Note.G), new PianoKey(Note.G),
new PianoKey(Note["G#"]), new PianoKey(Note['G#']),
new PianoKey(Note.A), new PianoKey(Note.A),
new PianoKey(Note["A#"]), new PianoKey(Note['A#']),
new PianoKey(Note.B), new PianoKey(Note.B),
]; ];

View File

@@ -1,10 +1,10 @@
import Model from "./Model"; import Model from './Model';
interface SearchHistory extends Model { interface SearchHistory extends Model {
query: string; query: string;
type: "song" | "artist" | "album" | "genre"; type: 'song' | 'artist' | 'album' | 'genre';
userId: number; userId: number;
timestamp: Date; timestamp: Date;
} }
export default SearchHistory; export default SearchHistory;

View File

@@ -1,4 +1,5 @@
type Skill = 'rhythm' type Skill =
| 'rhythm'
| 'two-hands' | 'two-hands'
| 'combos' | 'combos'
| 'arpeggio' | 'arpeggio'
@@ -9,7 +10,6 @@ type Skill = 'rhythm'
| 'chord-complexity' | 'chord-complexity'
| 'chord-timing' | 'chord-timing'
| 'pedal' | 'pedal'
| 'precision' | 'precision';
export default Skill;
export default Skill;

View File

@@ -1,6 +1,6 @@
import Model from "./Model"; import Model from './Model';
import SongDetails from "./SongDetails"; import SongDetails from './SongDetails';
import Artist from "./Artist"; import Artist from './Artist';
interface Song extends Model { interface Song extends Model {
id: number; id: number;
@@ -16,4 +16,4 @@ export interface SongWithArtist extends Song {
artist: Artist; artist: Artist;
} }
export default Song; export default Song;

View File

@@ -1,17 +1,17 @@
interface SongDetails { interface SongDetails {
length: number, length: number;
rhythm: number, rhythm: number;
arppegio: number, arppegio: number;
distance: number, distance: number;
lefthand: number, lefthand: number;
righthand: number, righthand: number;
twohands: number, twohands: number;
notecombo: number, notecombo: number;
precision: number, precision: number;
pedalpoint: number, pedalpoint: number;
chordtiming: number, chordtiming: number;
leadheadchange: number, leadheadchange: number;
chordcomplexity: number, chordcomplexity: number;
} }
export default SongDetails; export default SongDetails;

View File

@@ -5,4 +5,4 @@ interface SongHistory {
difficulties: JSON; difficulties: JSON;
} }
export default SongHistory; export default SongHistory;

View File

@@ -1,6 +1,6 @@
import UserData from "./UserData"; import UserData from './UserData';
import Model from "./Model"; import Model from './Model';
import UserSettings from "./UserSettings"; import UserSettings from './UserSettings';
interface User extends Model { interface User extends Model {
name: string; name: string;
@@ -11,4 +11,4 @@ interface User extends Model {
settings: UserSettings; settings: UserSettings;
} }
export default User; export default User;

View File

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

View File

@@ -1,14 +1,14 @@
interface UserSettings { interface UserSettings {
notifications: { notifications: {
pushNotif: boolean, pushNotif: boolean;
emailNotif: boolean, emailNotif: boolean;
trainNotif: boolean, trainNotif: boolean;
newSongNotif: boolean newSongNotif: boolean;
}, };
weeklyReport: boolean, weeklyReport: boolean;
leaderBoard: boolean, leaderBoard: boolean;
showActivity: boolean, showActivity: boolean;
recommendations: boolean recommendations: boolean;
} }
export default UserSettings export default UserSettings;

View File

@@ -1,102 +1,112 @@
{ {
"name": "chromacase", "name": "chromacase",
"version": "1.0.0", "version": "1.0.0",
"main": "node_modules/expo/AppEntry.js", "main": "node_modules/expo/AppEntry.js",
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"android": "expo start --android", "android": "expo start --android",
"ios": "expo start --ios", "ios": "expo start --ios",
"web": "expo start --web", "web": "expo start --web",
"eject": "expo eject", "eject": "expo eject",
"test": "jest -i", "pretty:check": "prettier --check",
"test:cov": "jest -i --coverage", "pretty:write": "prettier --write",
"test:watch": "jest -i --watch", "lint": "eslint .",
"storybook": "start-storybook -p 6006", "test": "jest -i",
"build-storybook": "build-storybook", "test:cov": "jest -i --coverage",
"chromatic": "chromatic --exit-zero-on-changes" "test:watch": "jest -i --watch",
}, "storybook": "start-storybook -p 6006",
"dependencies": { "build-storybook": "build-storybook",
"@expo/vector-icons": "^13.0.0", "chromatic": "chromatic --exit-zero-on-changes"
"@motiz88/react-native-midi": "^0.0.5", },
"@react-native-async-storage/async-storage": "~1.17.3", "dependencies": {
"@react-navigation/native": "^6.0.11", "@expo/vector-icons": "^13.0.0",
"@react-navigation/native-stack": "^6.7.0", "@motiz88/react-native-midi": "^0.0.5",
"@reduxjs/toolkit": "^1.8.3", "@react-native-async-storage/async-storage": "~1.17.3",
"@tanstack/react-query": "^4.2.3", "@react-navigation/native": "^6.0.11",
"@types/jest": "^28.1.4", "@react-navigation/native-stack": "^6.7.0",
"@types/react-dom": "~18.0.8", "@reduxjs/toolkit": "^1.8.3",
"@types/react-query": "^1.2.9", "@tanstack/react-query": "^4.2.3",
"@types/react-test-renderer": "^18.0.0", "@types/jest": "^28.1.4",
"add": "^2.0.6", "@types/react-dom": "~18.0.8",
"expo": "^47.0.8", "@types/react-query": "^1.2.9",
"expo-asset": "~8.7.0", "@types/react-test-renderer": "^18.0.0",
"expo-dev-client": "~2.0.1", "add": "^2.0.6",
"expo-linking": "~3.3.1", "expo": "^47.0.8",
"expo-screen-orientation": "~5.0.1", "expo-asset": "~8.7.0",
"expo-secure-store": "~12.0.0", "expo-dev-client": "~2.0.1",
"expo-splash-screen": "~0.17.5", "expo-linking": "~3.3.1",
"expo-status-bar": "~1.4.2", "expo-screen-orientation": "~5.0.1",
"format-duration": "^2.0.0", "expo-secure-store": "~12.0.0",
"i18next": "^21.8.16", "expo-splash-screen": "~0.17.5",
"install": "^0.13.0", "expo-status-bar": "~1.4.2",
"jest": "^26.6.3", "format-duration": "^2.0.0",
"jest-expo": "^45.0.1", "i18next": "^21.8.16",
"midi-player-js": "^2.0.16", "install": "^0.13.0",
"moti": "^0.22.0", "jest": "^26.6.3",
"native-base": "^3.4.17", "jest-expo": "^45.0.1",
"opensheetmusicdisplay": "^1.7.5", "midi-player-js": "^2.0.16",
"react": "18.1.0", "moti": "^0.22.0",
"react-dom": "18.1.0", "native-base": "^3.4.17",
"react-i18next": "^11.18.3", "opensheetmusicdisplay": "^1.7.5",
"react-native": "0.70.5", "react": "18.1.0",
"react-native-paper": "^4.12.5", "react-dom": "18.1.0",
"react-native-reanimated": "~2.12.0", "react-i18next": "^11.18.3",
"react-native-safe-area-context": "4.4.1", "react-native": "0.70.5",
"react-native-screens": "~3.18.0", "react-native-paper": "^4.12.5",
"react-native-super-grid": "^4.6.1", "react-native-reanimated": "~2.12.0",
"react-native-svg": "13.4.0", "react-native-safe-area-context": "4.4.1",
"react-native-testing-library": "^6.0.0", "react-native-screens": "~3.18.0",
"react-native-url-polyfill": "^1.3.0", "react-native-super-grid": "^4.6.1",
"react-native-web": "~0.18.7", "react-native-svg": "13.4.0",
"react-redux": "^8.0.2", "react-native-testing-library": "^6.0.0",
"react-timer-hook": "^3.0.5", "react-native-url-polyfill": "^1.3.0",
"react-use-precision-timer": "^3.3.1", "react-native-web": "~0.18.7",
"redux-persist": "^6.0.0", "react-redux": "^8.0.2",
"soundfont-player": "^0.12.0", "react-timer-hook": "^3.0.5",
"standardized-audio-context": "^25.3.51", "react-use-precision-timer": "^3.3.1",
"type-fest": "^3.6.0", "redux-persist": "^6.0.0",
"yup": "^0.32.11" "soundfont-player": "^0.12.0",
}, "standardized-audio-context": "^25.3.51",
"devDependencies": { "type-fest": "^3.6.0",
"@babel/core": "^7.19.3", "yup": "^0.32.11"
"@babel/plugin-proposal-export-namespace-from": "^7.18.9", },
"@expo/webpack-config": "^0.17.4", "devDependencies": {
"@storybook/addon-actions": "^6.5.15", "@babel/core": "^7.19.3",
"@storybook/addon-essentials": "^6.5.15", "@babel/plugin-proposal-export-namespace-from": "^7.18.9",
"@storybook/addon-interactions": "^6.5.15", "@expo/webpack-config": "^0.17.4",
"@storybook/addon-links": "^6.5.15", "@storybook/addon-actions": "^6.5.15",
"@storybook/builder-webpack4": "^6.5.15", "@storybook/addon-essentials": "^6.5.15",
"@storybook/manager-webpack4": "^6.5.15", "@storybook/addon-interactions": "^6.5.15",
"@storybook/react": "^6.5.15", "@storybook/addon-links": "^6.5.15",
"@storybook/testing-library": "^0.0.13", "@storybook/builder-webpack4": "^6.5.15",
"@testing-library/react-native": "^11.0.0", "@storybook/manager-webpack4": "^6.5.15",
"@types/node": "^18.11.8", "@storybook/react": "^6.5.15",
"@types/react": "~18.0.24", "@storybook/testing-library": "^0.0.13",
"@types/react-native": "~0.70.6", "@testing-library/react-native": "^11.0.0",
"@types/react-navigation": "^3.4.0", "@types/node": "^18.11.8",
"babel-loader": "^8.3.0", "@types/react": "~18.0.24",
"babel-plugin-transform-inline-environment-variables": "^0.4.4", "@types/react-native": "~0.70.6",
"chromatic": "^6.14.0", "@types/react-navigation": "^3.4.0",
"react-test-renderer": "17.0.2", "babel-loader": "^8.3.0",
"typescript": "^4.6.3" "babel-plugin-transform-inline-environment-variables": "^0.4.4",
}, "chromatic": "^6.14.0",
"private": true, "@typescript-eslint/eslint-plugin": "^5.43.0",
"jest": { "@typescript-eslint/parser": "^5.0.0",
"preset": "jest-expo", "eslint": "^8.42.0",
"transformIgnorePatterns": [ "eslint-config-prettier": "^8.3.0",
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)" "eslint-plugin-prettier": "^4.0.0",
] "eslint-plugin-react": "^7.31.11",
}, "prettier": "^2.8.8",
"readme": "ERROR: No README data found!", "react-test-renderer": "17.0.2",
"_id": "chromacase@1.0.0" "typescript": "^4.6.3"
},
"private": true,
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)"
]
},
"readme": "ERROR: No README data found!",
"_id": "chromacase@1.0.0"
} }

View File

@@ -1,11 +1,10 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AvailableLanguages, DefaultLanguage } from "../i18n/i18n"; import { AvailableLanguages, DefaultLanguage } from '../i18n/i18n';
export const languageSlice = createSlice({ export const languageSlice = createSlice({
name: 'language', name: 'language',
initialState: { initialState: {
value: DefaultLanguage value: DefaultLanguage,
}, },
reducers: { reducers: {
useLanguage: (state, action: PayloadAction<AvailableLanguages>) => { useLanguage: (state, action: PayloadAction<AvailableLanguages>) => {
@@ -17,4 +16,4 @@ export const languageSlice = createSlice({
}, },
}); });
export const { useLanguage, resetLanguage } = languageSlice.actions; export const { useLanguage, resetLanguage } = languageSlice.actions;
export default languageSlice.reducer; export default languageSlice.reducer;

View File

@@ -1,5 +1,5 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import LocalSettings from "../models/LocalSettings"; import LocalSettings from '../models/LocalSettings';
export const settingsSlice = createSlice({ export const settingsSlice = createSlice({
name: 'settings', name: 'settings',
@@ -12,14 +12,14 @@ export const settingsSlice = createSlice({
difficulty: 'beg', difficulty: 'beg',
colorBlind: false, colorBlind: false,
customAds: true, customAds: true,
dataCollection: true dataCollection: true,
}, },
}, },
reducers: { reducers: {
updateSettings: (state, action: PayloadAction<Partial<LocalSettings>>) => { updateSettings: (state, action: PayloadAction<Partial<LocalSettings>>) => {
state.local = { ...state.local, ...action.payload }; state.local = { ...state.local, ...action.payload };
} },
} },
}); });
export const { updateSettings } = settingsSlice.actions; export const { updateSettings } = settingsSlice.actions;
export default settingsSlice.reducer; export default settingsSlice.reducer;

View File

@@ -2,42 +2,55 @@ import userReducer from '../state/UserSlice';
import settingsReduder from './SettingsSlice'; import settingsReduder from './SettingsSlice';
import { StateFromReducersMapObject, configureStore } from '@reduxjs/toolkit'; import { StateFromReducersMapObject, configureStore } from '@reduxjs/toolkit';
import languageReducer from './LanguageSlice'; import languageReducer from './LanguageSlice';
import { TypedUseSelectorHook, useDispatch as reduxDispatch, useSelector as reduxSelector } from 'react-redux' import {
import { persistStore, persistCombineReducers, FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from "redux-persist"; TypedUseSelectorHook,
useDispatch as reduxDispatch,
useSelector as reduxSelector,
} from 'react-redux';
import {
persistStore,
persistCombineReducers,
FLUSH,
PAUSE,
PERSIST,
PURGE,
REGISTER,
REHYDRATE,
} from 'redux-persist';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import { CurriedGetDefaultMiddleware } from '@reduxjs/toolkit/dist/getDefaultMiddleware'; import { CurriedGetDefaultMiddleware } from '@reduxjs/toolkit/dist/getDefaultMiddleware';
import { PersistPartial } from 'redux-persist/es/persistReducer'; import { PersistPartial } from 'redux-persist/es/persistReducer';
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage: AsyncStorage storage: AsyncStorage,
} };
const reducers = { const reducers = {
user: userReducer, user: userReducer,
language: languageReducer, language: languageReducer,
settings: settingsReduder settings: settingsReduder,
} };
type State = StateFromReducersMapObject<typeof reducers>; type State = StateFromReducersMapObject<typeof reducers>;
let store = configureStore({ const store = configureStore({
reducer: persistCombineReducers(persistConfig, reducers), reducer: persistCombineReducers(persistConfig, reducers),
middleware: (getDefaultMiddleware: CurriedGetDefaultMiddleware<State & PersistPartial>) => middleware: (getDefaultMiddleware: CurriedGetDefaultMiddleware<State & PersistPartial>) =>
getDefaultMiddleware({ getDefaultMiddleware({
serializableCheck: { serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER], ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
}, },
}), }),
}) });
let persistor = persistStore(store); const persistor = persistStore(store);
// Infer the `RootState` and `AppDispatch` types from the store itself // Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState> export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch; export type AppDispatch = typeof store.dispatch;
// Use throughout your app instead of plain `useDispatch` and `useSelector` // Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useDispatch: () => AppDispatch = reduxDispatch export const useDispatch: () => AppDispatch = reduxDispatch;
export const useSelector: TypedUseSelectorHook<RootState> = reduxSelector export const useSelector: TypedUseSelectorHook<RootState> = reduxSelector;
export default store export default store;
export { persistor } export { persistor };

View File

@@ -2,18 +2,18 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AccessToken } from '../API'; import { AccessToken } from '../API';
export const userSlice = createSlice({ export const userSlice = createSlice({
name: 'user', name: 'user',
initialState: { initialState: {
accessToken: undefined as AccessToken | undefined accessToken: undefined as AccessToken | undefined,
}, },
reducers: { reducers: {
setAccessToken: (state, action: PayloadAction<AccessToken>) => { setAccessToken: (state, action: PayloadAction<AccessToken>) => {
state.accessToken = action.payload; state.accessToken = action.payload;
}, },
unsetAccessToken: (state) => { unsetAccessToken: (state) => {
state.accessToken = undefined; state.accessToken = undefined;
}, },
}, },
}); });
export const { setAccessToken, unsetAccessToken } = userSlice.actions; export const { setAccessToken, unsetAccessToken } = userSlice.actions;
export default userSlice.reducer; export default userSlice.reducer;

View File

@@ -1,107 +1,117 @@
{ {
"extends": "expo/tsconfig.base", "extends": "expo/tsconfig.base",
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Enable incremental compilation */ // "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* Language and Environment */
"target": "esnext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": ["es2019", "DOM"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ "lib": [
"jsx": "react-native", /* Specify what JSX code is generated. */ "es2019",
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ "DOM"
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ "jsx": "react-native" /* Specify what JSX code is generated. */,
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */ /* Modules */
"module": "commonjs", /* Specify what module code is generated. */ "module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": ["react-native", "jest", "node"], /* Specify type package names to be included without being referenced in a source file. */ "types": [
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ "react-native",
"resolveJsonModule": true, /* Enable importing .json files */ "jest",
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */ "node"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ "allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */ // "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */ // "removeComments": true, /* Disable emitting comments. */
"noEmit": true, /* Disable emitting files from a compilation. */ "noEmit": true /* Disable emitting files from a compilation. */,
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */ // "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */ /* Interop Constraints */
"isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ "allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */ /* Type Checking */
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */,
"strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ "strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */,
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */,
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
"noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ "noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */,
"useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ "useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */,
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ "alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
"noUnusedLocals": false, /* Enable error reporting when a local variables aren't read. */ "noUnusedLocals": false /* Enable error reporting when a local variables aren't read. */,
"noUnusedParameters": false, /* Raise an error when a function parameter isn't read */ "noUnusedParameters": false /* Raise an error when a function parameter isn't read */,
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
"noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */,
"noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ "noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */,
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */,
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
}, },
"exclude": [ "exclude": [
"node_modules", "babel.config.js", "metro.config.js", "node_modules",
"jest.config.js", "app.config.ts", "babel.config.js",
"*/*.test.tsx" "metro.config.js",
] "jest.config.js",
"app.config.ts",
"*/*.test.tsx"
]
} }

View File

@@ -1,42 +1,46 @@
import { VStack, Text, Image, Heading, IconButton, Icon, Container } from 'native-base'; import { VStack, Image, Heading, IconButton, Icon, Container } from 'native-base';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
import { SafeAreaView } from 'react-native'; import { SafeAreaView } from 'react-native';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import LoadingComponent from '../components/Loading'; import LoadingComponent from '../components/Loading';
import API from '../API'; import API from '../API';
const handleFavorite = () => { const handleFavorite = () => {};
type ArtistDetailsViewProps = {
artistId: number;
}; };
const ArtistDetailsView = ({ artistId }: any) => { const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => {
const { isLoading, data: artistData, error } = useQuery(['artist', artistId], () => API.getArtist(artistId)); const { isLoading, data: artistData } = useQuery(['artist', artistId], () =>
API.getArtist(artistId)
);
if (isLoading) { if (isLoading) {
return <LoadingComponent />; return <LoadingComponent />;
} }
return ( return (
<SafeAreaView> <SafeAreaView>
<Container m={3}> <Container m={3}>
<Image <Image
source={{ uri: 'https://picsum.photos/200' }} source={{ uri: 'https://picsum.photos/200' }}
alt={artistData?.name} alt={artistData?.name}
size={20} size={20}
borderRadius="full" borderRadius="full"
/> />
<VStack space={3}> <VStack space={3}>
<Heading>{artistData?.name}</Heading> <Heading>{artistData?.name}</Heading>
<IconButton <IconButton
icon={<Icon as={Ionicons} name="heart" size={6} color="red.500" />} icon={<Icon as={Ionicons} name="heart" size={6} color="red.500" />}
onPress={() => handleFavorite()} onPress={() => handleFavorite()}
variant="unstyled" variant="unstyled"
_pressed={{ opacity: 0.6 }} _pressed={{ opacity: 0.6 }}
/> />
</VStack> </VStack>
</Container> </Container>
</SafeAreaView> </SafeAreaView>
); );
}; };
export default ArtistDetailsView; export default ArtistDetailsView;

View File

@@ -6,8 +6,12 @@ import store from '../state/Store';
import AuthenticationView from '../views/AuthenticationView'; import AuthenticationView from '../views/AuthenticationView';
describe('<AuthenticationView />', () => { describe('<AuthenticationView />', () => {
it('has 3 children', () => { it('has 3 children', () => {
const tree = TestRenderer.create(<Provider store={store}><AuthenticationView /></Provider>).toJSON(); const tree = TestRenderer.create(
expect(tree.children.length).toBe(3); <Provider store={store}>
}); <AuthenticationView />
}); </Provider>
).toJSON();
expect(tree.children.length).toBe(3);
});
});

View File

@@ -1,58 +1,88 @@
import React from "react"; import React from 'react';
import { useDispatch } from '../state/Store'; import { useDispatch } from '../state/Store';
import { Translate, translate } from "../i18n/i18n"; import { Translate, translate } from '../i18n/i18n';
import API, { APIError } from "../API"; import API, { APIError } from '../API';
import { setAccessToken } from "../state/UserSlice"; import { setAccessToken } from '../state/UserSlice';
import { Center, Button, Text } from 'native-base'; import { Center, Button, Text } from 'native-base';
import SigninForm from "../components/forms/signinform"; import SigninForm from '../components/forms/signinform';
import SignupForm from "../components/forms/signupform"; import SignupForm from '../components/forms/signupform';
import TextButton from "../components/TextButton"; import TextButton from '../components/TextButton';
import { RouteProps } from "../Navigation"; import { RouteProps } from '../Navigation';
const hanldeSignin = async (username: string, password: string, apiSetter: (accessToken: string) => void): Promise<string> => { const hanldeSignin = async (
username: string,
password: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try { try {
const apiAccess = await API.authenticate({ username, password }); const apiAccess = await API.authenticate({ username, password });
apiSetter(apiAccess); apiSetter(apiAccess);
return translate("loggedIn"); return translate('loggedIn');
} catch (error) { } catch (error) {
if (error instanceof APIError) return translate(error.userMessage); if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message; if (error instanceof Error) return error.message;
return translate("unknownError"); return translate('unknownError');
} }
}; };
const handleSignup = async (username: string, password: string, email: string, apiSetter: (accessToken: string) => void): Promise<string> => { const handleSignup = async (
username: string,
password: string,
email: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try { try {
const apiAccess = await API.createAccount({ username, password, email }); const apiAccess = await API.createAccount({ username, password, email });
apiSetter(apiAccess); apiSetter(apiAccess);
return translate("loggedIn"); return translate('loggedIn');
} catch (error) { } catch (error) {
if (error instanceof APIError) return translate(error.userMessage); if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message; if (error instanceof Error) return error.message;
return translate("unknownError"); return translate('unknownError');
} }
}; };
type AuthenticationViewProps = { type AuthenticationViewProps = {
isSignup: boolean; isSignup: boolean;
} };
const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) => { const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [mode, setMode] = React.useState<"signin" | "signup">(isSignup ? "signup" : "signin"); const [mode, setMode] = React.useState<'signin' | 'signup'>(isSignup ? 'signup' : 'signin');
return ( return (
<Center style={{ flex: 1 }}> <Center style={{ flex: 1 }}>
<Text><Translate translationKey='welcome'/></Text> <Text>
{mode === "signin" <Translate translationKey="welcome" />
? <SigninForm onSubmit={(username, password) => hanldeSignin(username, password, (accessToken) => dispatch(setAccessToken(accessToken)))} /> </Text>
: <SignupForm onSubmit={(username, password, email) => handleSignup(username, password, email, (accessToken) => dispatch(setAccessToken(accessToken)))} /> {mode === 'signin' ? (
} <SigninForm
{ mode ==="signin" && <Button variant="outline" marginTop={5} colorScheme="error" >{translate("forgottenPassword")}</Button> } onSubmit={(username, password) =>
hanldeSignin(username, password, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
/>
) : (
<SignupForm
onSubmit={(username, password, email) =>
handleSignup(username, password, email, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
/>
)}
{mode === 'signin' && (
<Button variant="outline" marginTop={5} colorScheme="error">
{translate('forgottenPassword')}
</Button>
)}
<TextButton <TextButton
translate={{ translationKey: mode === "signin" ? "signUpBtn" : "signInBtn" }} translate={{ translationKey: mode === 'signin' ? 'signUpBtn' : 'signInBtn' }}
variant='outline' marginTop={5} colorScheme='primary' variant="outline"
onPress={() => setMode(mode === "signin" ? "signup" : "signin")} marginTop={5}
colorScheme="primary"
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
/> />
</Center> </Center>
); );

View File

@@ -6,8 +6,12 @@ import store from '../state/Store';
import HomeView from '../views/HomeView'; import HomeView from '../views/HomeView';
describe('<HomeView />', () => { describe('<HomeView />', () => {
it('has 2 children', () => { it('has 2 children', () => {
const tree = TestRenderer.create(<Provider store={store}><HomeView /></Provider>).toJSON(); const tree = TestRenderer.create(
expect(tree.children.length).toBe(2); <Provider store={store}>
}); <HomeView />
</Provider>
).toJSON();
expect(tree.children.length).toBe(2);
});
}); });

View File

@@ -1,40 +1,19 @@
import React from "react"; import React from 'react';
import { useQueries, useQuery } from "react-query"; import { useQueries, useQuery } from 'react-query';
import API from "../API"; import API from '../API';
import LoadingComponent from "../components/Loading"; import { LoadingView } from '../components/Loading';
import CardGridCustom from "../components/CardGridCustom"; import { Box, ScrollView, Flex, Stack, Heading, VStack, HStack } from 'native-base';
import { LoadingView } from "../components/Loading"; import { useNavigation } from '../Navigation';
import { import SongCardGrid from '../components/SongCardGrid';
Center, import CompetenciesTable from '../components/CompetenciesTable';
Box, import ProgressBar from '../components/ProgressBar';
ScrollView, import Translate from '../components/Translate';
Flex, import TextButton from '../components/TextButton';
useBreakpointValue, import Song from '../models/Song';
Stack, import { FontAwesome5 } from '@expo/vector-icons';
Heading,
Container,
VStack,
HStack,
Column,
Button,
Text,
useTheme
} from "native-base";
import { useNavigation } from "../Navigation";
import SongCardGrid from "../components/SongCardGrid";
import CompetenciesTable from "../components/CompetenciesTable";
import ProgressBar from "../components/ProgressBar";
import Translate from "../components/Translate";
import TextButton from "../components/TextButton";
import SearchHistoryCard from "../components/HistoryCard";
import Song from "../models/Song";
import { FontAwesome5 } from "@expo/vector-icons";
const HomeView = () => { const HomeView = () => {
const theme = useTheme();
const navigation = useNavigation(); const navigation = useNavigation();
const screenSize = useBreakpointValue({ base: 'small', md: "big"});
const userQuery = useQuery(['user'], () => API.getUserInfo()); const userQuery = useQuery(['user'], () => API.getUserInfo());
const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory()); const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory());
const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory(0, 10)); const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory(0, 10));
@@ -43,125 +22,168 @@ const HomeView = () => {
const songHistory = useQueries( const songHistory = useQueries(
playHistoryQuery.data?.map(({ songID }) => ({ playHistoryQuery.data?.map(({ songID }) => ({
queryKey: ['song', songID], queryKey: ['song', songID],
queryFn: () => API.getSong(songID) queryFn: () => API.getSong(songID),
})) ?? [] })) ?? []
); );
const artistsQueries = useQueries((songHistory const artistsQueries = useQueries(
.map((entry) => entry.data) songHistory
.concat(nextStepQuery.data ?? []) .map((entry) => entry.data)
.filter((s): s is Song => s !== undefined)) .concat(nextStepQuery.data ?? [])
.map((song) => ( .filter((s): s is Song => s !== undefined)
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.artistId) } .map((song) => ({
)) queryKey: ['artist', song.id],
queryFn: () => API.getArtist(song.artistId),
}))
); );
if (!userQuery.data || !skillsQuery.data || !searchHistoryQuery.data || !playHistoryQuery.data) { if (
return <LoadingView/> !userQuery.data ||
!skillsQuery.data ||
!searchHistoryQuery.data ||
!playHistoryQuery.data
) {
return <LoadingView />;
} }
return <ScrollView p={10}> return (
<Flex> <ScrollView p={10}>
<Stack space={4} <Flex>
display={{ base: 'block', md: 'flex' }} <Stack
direction={{ base: 'column', md: 'row' }} space={4}
textAlign={{ base: 'center', md: 'inherit' }} display={{ base: 'block', md: 'flex' }}
justifyContent="space-evenly" direction={{ base: 'column', md: 'row' }}
> textAlign={{ base: 'center', md: 'inherit' }}
<Translate fontSize="xl" flex={2} justifyContent="space-evenly"
translationKey="welcome" format={(welcome) => `${welcome} ${userQuery.data.name}!`} >
/> <Translate
<Box flex={1}> fontSize="xl"
<ProgressBar xp={userQuery.data.data.xp}/> flex={2}
</Box> translationKey="welcome"
</Stack> format={(welcome) => `${welcome} ${userQuery.data.name}!`}
</Flex> />
<Stack direction={{ base: 'column', lg: 'row' }} height="100%" space={5} paddingTop={5}> <Box flex={1}>
<VStack flex={{ lg: 2 }} space={5}> <ProgressBar xp={userQuery.data.data.xp} />
<SongCardGrid
heading={<Translate translationKey='goNextStep'/>}
songs={nextStepQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
cover: song.cover,
name: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
<Stack direction={{ base: 'column', lg: 'row' }}>
<Box flex={{ lg: 1 }}>
<Heading><Translate translationKey='mySkillsToImprove'/></Heading>
<Box padding={5}>
<CompetenciesTable {...skillsQuery.data}/>
</Box>
</Box> </Box>
<Box flex={{ lg: 1 }}> </Stack>
<SongCardGrid </Flex>
heading={<Translate translationKey='recentlyPlayed'/>} <Stack direction={{ base: 'column', lg: 'row' }} height="100%" space={5} paddingTop={5}>
songs={songHistory <VStack flex={{ lg: 2 }} space={5}>
.map(({ data }) => data) <SongCardGrid
.filter((data): data is Song => data !== undefined) heading={<Translate translationKey="goNextStep" />}
.filter((song, i, array) => array.map((s) => s.id).findIndex((id) => id == song.id) == i) songs={
.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)) nextStepQuery.data
?.filter((song) =>
artistsQueries.find(
(artistQuery) => artistQuery.data?.id === song.artistId
)
)
.map((song) => ({ .map((song) => ({
cover: song.cover, cover: song.cover,
name: song.name, name: song.name,
songId: song.id, songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name artistName: artistsQueries.find(
(artistQuery) => artistQuery.data?.id === song.artistId
)!.data!.name,
})) ?? [] })) ?? []
} }
/>
<Stack direction={{ base: 'column', lg: 'row' }}>
<Box flex={{ lg: 1 }}>
<Heading>
<Translate translationKey="mySkillsToImprove" />
</Heading>
<Box padding={5}>
<CompetenciesTable {...skillsQuery.data} />
</Box>
</Box>
<Box flex={{ lg: 1 }}>
<SongCardGrid
heading={<Translate translationKey="recentlyPlayed" />}
songs={
songHistory
.map(({ data }) => data)
.filter((data): data is Song => data !== undefined)
.filter(
(song, i, array) =>
array
.map((s) => s.id)
.findIndex((id) => id == song.id) == i
)
.filter((song) =>
artistsQueries.find(
(artistQuery) =>
artistQuery.data?.id === song.artistId
)
)
.map((song) => ({
cover: song.cover,
name: song.name,
songId: song.id,
artistName: artistsQueries.find(
(artistQuery) =>
artistQuery.data?.id === song.artistId
)!.data!.name,
})) ?? []
}
/>
</Box>
</Stack>
</VStack>
<VStack flex={{ lg: 1 }} height={{ lg: '100%' }} alignItems="center">
<HStack width="100%" justifyContent="space-evenly" p={5} space={5}>
<TextButton
translate={{ translationKey: 'searchBtn' }}
colorScheme="secondary"
size="sm"
onPress={() => navigation.navigate('Search', {})}
/> />
</Box> <TextButton
</Stack> translate={{ translationKey: 'settingsBtn' }}
</VStack> colorScheme="gray"
<VStack flex={{ lg: 1 }} height={{ lg: '100%' }} alignItems="center"> size="sm"
<HStack width="100%" justifyContent="space-evenly" p={5} space={5}> onPress={() => navigation.navigate('Settings')}
<TextButton />
translate={{ translationKey: 'searchBtn' }} </HStack>
colorScheme='secondary' size="sm" <Box style={{ width: '100%' }}>
onPress={() => navigation.navigate('Search', {})} <Heading>
/> <Translate translationKey="recentSearches" />
<TextButton translate={{ translationKey: 'settingsBtn' }} </Heading>
colorScheme='gray' size="sm" <Flex
onPress={() => navigation.navigate('Settings')} padding={3}
/> style={{
</HStack> width: '100%',
<Box style={{ width: '100%' }}> alignItems: 'flex-start',
<Heading><Translate translationKey='recentSearches'/></Heading> alignContent: 'flex-start',
<Flex padding={3} style={{ flexDirection: 'row',
width: '100%', flexWrap: 'wrap',
alignItems: 'flex-start', }}
alignContent: 'flex-start', >
flexDirection: 'row', {searchHistoryQuery.data?.length === 0 && (
flexWrap: 'wrap', <Translate translationKey="noRecentSearches" />
}}> )}
{ {[...new Set(searchHistoryQuery.data.map((x) => x.query))]
searchHistoryQuery.data?.length === 0 && <Translate translationKey='noRecentSearches'/> .slice(0, 5)
} .map((query) => (
{ <TextButton
[...(new Set(searchHistoryQuery.data.map((x) => x.query)))].slice(0, 5).map((query) => ( leftIcon={<FontAwesome5 name="search" size={16} />}
<TextButton style={{
leftIcon={ margin: 2,
<FontAwesome5 name="search" size={16} /> }}
} key={query}
style={{ variant="solid"
margin: 2, size="xs"
}} colorScheme="primary"
key={ query } label={query}
variant="solid" onPress={() =>
size="xs" navigation.navigate('Search', { query: query })
colorScheme="primary" }
label={query} />
onPress={() => navigation.navigate('Search', { query: query })} ))}
/> </Flex>
)) </Box>
} </VStack>
</Flex> </Stack>
</Box> </ScrollView>
</VStack> );
</Stack> };
</ScrollView>
}
export default HomeView; export default HomeView;

View File

@@ -1,10 +1,22 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { SafeAreaView, Platform, Animated } from 'react-native'; import { SafeAreaView, Platform, Animated } from 'react-native';
import * as ScreenOrientation from 'expo-screen-orientation'; import * as ScreenOrientation from 'expo-screen-orientation';
import { Box, Center, Column, Progress, Text, Row, View, useToast, Icon, HStack } from 'native-base'; import {
Box,
Center,
Column,
Progress,
Text,
Row,
View,
useToast,
Icon,
HStack,
} from 'native-base';
import IconButton from '../components/IconButton'; import IconButton from '../components/IconButton';
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
import { RouteProps, useNavigation } from "../Navigation"; import { RouteProps, useNavigation } from '../Navigation';
import { useQuery } from 'react-query'; import { useQuery } from 'react-query';
import API from '../API'; import API from '../API';
import LoadingComponent, { LoadingView } from '../components/Loading'; import LoadingComponent, { LoadingView } from '../components/Loading';
@@ -13,39 +25,36 @@ import VirtualPiano from '../components/VirtualPiano/VirtualPiano';
import { strToKey, keyToStr, Note } from '../models/Piano'; import { strToKey, keyToStr, Note } from '../models/Piano';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../state/Store'; import { RootState } from '../state/Store';
import { Translate, translate } from '../i18n/i18n'; import { translate } from '../i18n/i18n';
import { ColorSchemeType } from 'native-base/lib/typescript/components/types'; import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
import { useStopwatch } from "react-use-precision-timer"; import { useStopwatch } from 'react-use-precision-timer';
import PartitionView from '../components/PartitionView'; import PartitionView from '../components/PartitionView';
import TextButton from '../components/TextButton'; import TextButton from '../components/TextButton';
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from "@motiz88/react-native-midi"; import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
import * as Linking from 'expo-linking'; import * as Linking from 'expo-linking';
import { URL } from 'url'; import { URL } from 'url';
import { url } from 'inspector';
type PlayViewProps = { type PlayViewProps = {
songId: number, songId: number;
type: 'practice' | 'normal' type: 'practice' | 'normal';
} };
type ScoreMessage = { type ScoreMessage = {
content: string, content: string;
color?: ColorSchemeType color?: ColorSchemeType;
} };
// this a hot fix this should be reverted soon // this a hot fix this should be reverted soon
let scoroBaseApiUrl = Constants.manifest?.extra?.scoroUrl; let scoroBaseApiUrl = Constants.manifest?.extra?.scoroUrl;
if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') { if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') {
Linking.getInitialURL().then((url) => { Linking.getInitialURL().then((url) => {
if (url !== null) { if (url !== null) {
const location = new URL(url); const location = new URL(url);
if (location.protocol === 'https:') { if (location.protocol === 'https:') {
scoroBaseApiUrl = "wss://" + location.host + "/ws"; scoroBaseApiUrl = 'wss://' + location.host + '/ws';
} else { } else {
scoroBaseApiUrl = "ws://" + location.host + "/ws"; scoroBaseApiUrl = 'ws://' + location.host + '/ws';
} }
} }
}); });
@@ -74,8 +83,9 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render
const [score, setScore] = useState(0); // Between 0 and 100 const [score, setScore] = useState(0); // Between 0 and 100
const fadeAnim = useRef(new Animated.Value(0)).current; const fadeAnim = useRef(new Animated.Value(0)).current;
const musixml = useQuery(["musixml", songId], () => const musixml = useQuery(
API.getSongMusicXML(songId).then((data) => new TextDecoder().decode(data)), ['musixml', songId],
() => API.getSongMusicXML(songId).then((data) => new TextDecoder().decode(data)),
{ staleTime: Infinity } { staleTime: Infinity }
); );
const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000; const getElapsedTime = () => stopwatch.getElapsedRunningTime() - 3000;
@@ -84,12 +94,14 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const onPause = () => { const onPause = () => {
stopwatch.pause(); stopwatch.pause();
setPause(true); setPause(true);
webSocket.current?.send(JSON.stringify({ webSocket.current?.send(
type: "pause", JSON.stringify({
paused: true, type: 'pause',
time: getElapsedTime() paused: true,
})); time: getElapsedTime(),
} })
);
};
const onResume = () => { const onResume = () => {
if (stopwatch.isStarted()) { if (stopwatch.isStarted()) {
stopwatch.resume(); stopwatch.resume();
@@ -97,17 +109,21 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
stopwatch.start(); stopwatch.start();
} }
setPause(false); setPause(false);
webSocket.current?.send(JSON.stringify({ webSocket.current?.send(
type: "pause", JSON.stringify({
paused: false, type: 'pause',
time: getElapsedTime() paused: false,
})); time: getElapsedTime(),
} })
);
};
const onEnd = () => { const onEnd = () => {
webSocket.current?.send(JSON.stringify({ webSocket.current?.send(
type: "end" JSON.stringify({
})); type: 'end',
} })
);
};
const onMIDISuccess = (access: MIDIAccess) => { const onMIDISuccess = (access: MIDIAccess) => {
const inputs = access.inputs; const inputs = access.inputs;
@@ -120,12 +136,14 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
let inputIndex = 0; let inputIndex = 0;
webSocket.current = new WebSocket(scoroBaseApiUrl); webSocket.current = new WebSocket(scoroBaseApiUrl);
webSocket.current.onopen = () => { webSocket.current.onopen = () => {
webSocket.current!.send(JSON.stringify({ webSocket.current!.send(
type: "start", JSON.stringify({
id: song.data!.id, type: 'start',
mode: type, id: song.data!.id,
bearer: accessToken mode: type,
})); bearer: accessToken,
})
);
}; };
webSocket.current.onmessage = (message) => { webSocket.current.onmessage = (message) => {
try { try {
@@ -137,7 +155,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const points = data.info.score; const points = data.info.score;
const maxPoints = data.info.max_score || 1; const maxPoints = data.info.max_score || 1;
setScore(Math.floor(Math.max(points, 0) * 100 / maxPoints)); setScore(Math.floor((Math.max(points, 0) * 100) / maxPoints));
let formattedMessage = ''; let formattedMessage = '';
let messageColor: ColorSchemeType | undefined; let messageColor: ColorSchemeType | undefined;
@@ -172,18 +190,18 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }
} };
inputs.forEach((input) => { inputs.forEach((input) => {
if (inputIndex != 0) { if (inputIndex != 0) {
return; return;
} }
input.onmidimessage = (message) => { input.onmidimessage = (message) => {
const { command, channel, note, velocity } = parseMidiMessage(message); const { command } = parseMidiMessage(message);
const keyIsPressed = command == 9;; const keyIsPressed = command == 9;
const keyCode = message.data[1]; const keyCode = message.data[1];
webSocket.current?.send( webSocket.current?.send(
JSON.stringify({ JSON.stringify({
type: keyIsPressed ? "note_on" : "note_off", type: keyIsPressed ? 'note_on' : 'note_off',
note: keyCode, note: keyCode,
id: song.data!.id, id: song.data!.id,
time: getElapsedTime(), time: getElapsedTime(),
@@ -192,22 +210,22 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
}; };
inputIndex++; inputIndex++;
}); });
} };
const onMIDIFailure = () => { const onMIDIFailure = () => {
toast.show({ description: `Failed to get MIDI access` }); toast.show({ description: `Failed to get MIDI access` });
} };
useEffect(() => { useEffect(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {}); ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
let interval = setInterval(() => { const interval = setInterval(() => {
setTime(() => getElapsedTime()) // Countdown setTime(() => getElapsedTime()); // Countdown
}, 1); }, 1);
return () => { return () => {
ScreenOrientation.unlockAsync().catch(() => {}); ScreenOrientation.unlockAsync().catch(() => {});
onEnd(); onEnd();
clearInterval(interval); clearInterval(interval);
} };
}, []); }, []);
useEffect(() => { useEffect(() => {
if (lastScoreMessage) { if (lastScoreMessage) {
@@ -231,109 +249,157 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
}, [song.data, partitionRendered]); }, [song.data, partitionRendered]);
if (!song.data || !musixml.data) { if (!song.data || !musixml.data) {
return <LoadingView/>; return <LoadingView />;
} }
return ( return (
<SafeAreaView style={{ flexGrow: 1, flexDirection: 'column' }}> <SafeAreaView style={{ flexGrow: 1, flexDirection: 'column' }}>
<HStack width="100%" justifyContent="center" p={3} style={{ position: 'absolute', top: 1 }}> <HStack
width="100%"
justifyContent="center"
p={3}
style={{ position: 'absolute', top: 1 }}
>
<Animated.View style={{ opacity: fadeAnim }}> <Animated.View style={{ opacity: fadeAnim }}>
<TextButton <TextButton
disabled disabled
label={lastScoreMessage?.content ?? ''} label={lastScoreMessage?.content ?? ''}
colorScheme={lastScoreMessage?.color} rounded='sm' colorScheme={lastScoreMessage?.color}
rounded="sm"
/> />
</Animated.View> </Animated.View>
</HStack> </HStack>
<View style={{ flexGrow: 1, justifyContent: 'center' }}> <View style={{ flexGrow: 1, justifyContent: 'center' }}>
<PartitionView file={musixml.data} <PartitionView
file={musixml.data}
onPartitionReady={() => setPartitionRendered(true)} onPartitionReady={() => setPartitionRendered(true)}
timestamp={Math.max(0, time)} timestamp={Math.max(0, time)}
onEndReached={() => { onEndReached={() => {
onEnd(); onEnd();
}} }}
/> />
{ !partitionRendered && <LoadingComponent/> } {!partitionRendered && <LoadingComponent />}
</View> </View>
{isVirtualPianoVisible && <Column {isVirtualPianoVisible && (
<Column
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
height: '20%',
width: '100%',
}}
>
<VirtualPiano
onNoteDown={(note) => {
console.log('On note down', keyToStr(note));
}}
onNoteUp={(note) => {
console.log('On note up', keyToStr(note));
}}
showOctaveNumbers={true}
startNote={Note.C}
endNote={Note.B}
startOctave={2}
endOctave={5}
style={{
width: '80%',
height: '100%',
}}
highlightedNotes={[
{ key: strToKey('D3') },
{ key: strToKey('A#'), bgColor: '#00FF00' },
]}
/>
</Column>
)}
<Box
shadow={4}
style={{ style={{
display: 'flex', height: '12%',
justifyContent: "flex-end",
alignItems: "center",
height: '20%',
width: '100%', width: '100%',
borderWidth: 0.5,
margin: 5,
display: !partitionRendered ? 'none' : undefined,
}} }}
> >
<VirtualPiano <Row justifyContent="space-between" style={{ flexGrow: 1, alignItems: 'center' }}>
onNoteDown={(note: any) => { <Column
console.log("On note down", keyToStr(note)); space={2}
}} style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}
onNoteUp={(note: any) => { >
console.log("On note up", keyToStr(note));
}}
showOctaveNumbers={true}
startNote={Note.C}
endNote={Note.B}
startOctave={2}
endOctave={5}
style={{
width: '80%',
height: '100%',
}}
highlightedNotes={
[
{ key: strToKey("D3") },
{ key: strToKey("A#"), bgColor: "#00FF00" },
]
}
/>
</Column>}
<Box shadow={4} style={{ height: '12%', width:'100%', borderWidth: 0.5, margin: 5, display: !partitionRendered ? 'none' : undefined }}>
<Row justifyContent='space-between' style={{ flexGrow: 1, alignItems: 'center' }} >
<Column space={2} style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontWeight: 'bold' }}>Score: {score}%</Text> <Text style={{ fontWeight: 'bold' }}>Score: {score}%</Text>
<Progress value={score} style={{ width: '90%' }}/> <Progress value={score} style={{ width: '90%' }} />
</Column> </Column>
<Center style={{ flex: 1, alignItems: 'center' }}> <Center style={{ flex: 1, alignItems: 'center' }}>
<Text style={{ fontWeight: '700' }}>{song.data.name}</Text> <Text style={{ fontWeight: '700' }}>{song.data.name}</Text>
</Center> </Center>
<Row style={{ flex: 1, height: '100%', justifyContent: 'space-evenly', alignItems: 'center' }}> <Row
{midiKeyboardFound && <> style={{
<IconButton size='sm' variant='solid' icon={ flex: 1,
<Icon as={Ionicons} name={paused ? "play" : "pause"}/> height: '100%',
} onPress={() => { justifyContent: 'space-evenly',
if (paused) { alignItems: 'center',
onResume(); }}
} else { >
onPause(); {midiKeyboardFound && (
} <>
}}/> <IconButton
<IconButton size='sm' colorScheme='coolGray' variant='solid' icon={ size="sm"
<Icon as={MaterialCommunityIcons} variant="solid"
name={ isVirtualPianoVisible ? "piano-off" : "piano"} /> icon={<Icon as={Ionicons} name={paused ? 'play' : 'pause'} />}
} onPress={() => { onPress={() => {
setVirtualPianoVisible(!isVirtualPianoVisible); if (paused) {
}}/> onResume();
<Text> } else {
{ time < 0 onPause();
? paused }
? '0:00' }}
: Math.floor((time % 60000) / 1000).toFixed(0).toString() />
: `${Math.floor(time / 60000)}:${Math.floor((time % 60000) / 1000).toFixed(0).toString().padStart(2, '0')}` <IconButton
} size="sm"
</Text> colorScheme="coolGray"
<IconButton size='sm' colorScheme='coolGray' variant='solid' icon={ variant="solid"
<Icon as={Ionicons} name="stop"/> icon={
} onPress={() => { <Icon
onEnd(); as={MaterialCommunityIcons}
}}/> name={isVirtualPianoVisible ? 'piano-off' : 'piano'}
</>} />
}
onPress={() => {
setVirtualPianoVisible(!isVirtualPianoVisible);
}}
/>
<Text>
{time < 0
? paused
? '0:00'
: Math.floor((time % 60000) / 1000)
.toFixed(0)
.toString()
: `${Math.floor(time / 60000)}:${Math.floor(
(time % 60000) / 1000
)
.toFixed(0)
.toString()
.padStart(2, '0')}`}
</Text>
<IconButton
size="sm"
colorScheme="coolGray"
variant="solid"
icon={<Icon as={Ionicons} name="stop" />}
onPress={() => {
onEnd();
}}
/>
</>
)}
</Row> </Row>
</Row> </Row>
</Box> </Box>
</SafeAreaView> </SafeAreaView>
); );
} };
export default PlayView export default PlayView;

View File

@@ -1,100 +1,131 @@
import React from 'react'; import React from 'react';
import { Dimensions, View } from 'react-native'; import { Dimensions, View } from 'react-native';
import { Box, Image, Heading, HStack, Card, Button, Spacer, Text } from 'native-base'; import { Box, Image, Heading, HStack, Card, Text } from 'native-base';
import Translate from '../components/Translate'; import Translate from '../components/Translate';
import { useNavigation } from "../Navigation"; import { useNavigation } from '../Navigation';
import TextButton from '../components/TextButton'; import TextButton from '../components/TextButton';
const UserMedals = () => { const UserMedals = () => {
return ( return (
<Card marginX={20} marginY={10}> <Card marginX={20} marginY={10}>
<Heading> <Heading>
<Translate translationKey='medals'/> <Translate translationKey="medals" />
</Heading> </Heading>
<HStack alignItems={'row'} space='10'> <HStack alignItems={'row'} space="10">
<Image source={{ <Image
uri: "https://wallpaperaccess.com/full/317501.jpg" source={{
}} alt="Profile picture" size="lg" uri: 'https://wallpaperaccess.com/full/317501.jpg',
/> }}
<Image source={{ alt="Profile picture"
uri: "https://wallpaperaccess.com/full/317501.jpg" size="lg"
}} alt="Profile picture" size="lg" />
/> <Image
<Image source={{ source={{
uri: "https://wallpaperaccess.com/full/317501.jpg" uri: 'https://wallpaperaccess.com/full/317501.jpg',
}} alt="Profile picture" size="lg" }}
/> alt="Profile picture"
<Image source={{ size="lg"
uri: "https://wallpaperaccess.com/full/317501.jpg" />
}} alt="Profile picture" size="lg" <Image
/> source={{
</HStack> uri: 'https://wallpaperaccess.com/full/317501.jpg',
</Card> }}
); alt="Profile picture"
} size="lg"
/>
<Image
source={{
uri: 'https://wallpaperaccess.com/full/317501.jpg',
}}
alt="Profile picture"
size="lg"
/>
</HStack>
</Card>
);
};
const PlayerStats = () => { const PlayerStats = () => {
const answer = "Answer from back"; const answer = 'Answer from back';
return( return (
<Card marginX={20} marginY={10}> <Card marginX={20} marginY={10}>
<Heading> <Translate translationKey='playerStats'/> </Heading> <Heading>
<Text> <Translate translationKey='mostPlayedSong'/> {answer} </Text> {' '}
<Text> <Translate translationKey='goodNotesPlayed'/> {answer} </Text> <Translate translationKey="playerStats" />{' '}
<Text> <Translate translationKey='longestCombo'/> {answer} </Text> </Heading>
<Text> <Translate translationKey='favoriteGenre'/> {answer} </Text> <Text>
</Card> {' '}
<Translate translationKey="mostPlayedSong" /> {answer}{' '}
); </Text>
} <Text>
{' '}
<Translate translationKey="goodNotesPlayed" /> {answer}{' '}
</Text>
<Text>
{' '}
<Translate translationKey="longestCombo" /> {answer}{' '}
</Text>
<Text>
{' '}
<Translate translationKey="favoriteGenre" /> {answer}{' '}
</Text>
</Card>
);
};
const ProfilePictureBannerAndLevel = () => { const ProfilePictureBannerAndLevel = () => {
const profilePic = "https://wallpaperaccess.com/full/317501.jpg" const profilePic = 'https://wallpaperaccess.com/full/317501.jpg';
const username = "Username" const username = 'Username';
const level = "1" const level = '1';
// banner size // banner size
const dimensions = Dimensions.get('window'); const dimensions = Dimensions.get('window');
const imageHeight = dimensions.height / 5; const imageHeight = dimensions.height / 5;
const imageWidth = dimensions.width; const imageWidth = dimensions.width;
// need to change the padding for the username and level // need to change the padding for the username and level
return ( return (
<View style={{flexDirection: 'row'}}> <View style={{ flexDirection: 'row' }}>
<Image source={{ uri : "https://wallpaperaccess.com/full/317501.jpg" }} size="lg" <Image
style={{ height: imageHeight, width: imageWidth, zIndex:0, opacity: 0.5 }} source={{ uri: 'https://wallpaperaccess.com/full/317501.jpg' }}
/> size="lg"
<Box zIndex={1} position={"absolute"} marginY={10} marginX={10}> style={{ height: imageHeight, width: imageWidth, zIndex: 0, opacity: 0.5 }}
<Image borderRadius={100} source={{ uri: profilePic }} />
alt="Profile picture" size="lg" <Box zIndex={1} position={'absolute'} marginY={10} marginX={10}>
style= {{position: 'absolute'}} <Image
borderRadius={100}
source={{ uri: profilePic }}
alt="Profile picture"
size="lg"
style={{ position: 'absolute' }}
/> />
<Box w="100%" paddingY={3} paddingLeft={100}> <Box w="100%" paddingY={3} paddingLeft={100}>
<Heading>{username}</Heading> <Heading>{username}</Heading>
<Heading>Level : {level}</Heading> <Heading>Level : {level}</Heading>
</Box> </Box>
</Box> </Box>
</View> </View>
); );
} };
const ProfileView = () => { const ProfileView = () => {
const navigation = useNavigation(); const navigation = useNavigation();
return ( return (
<View style={{flexDirection: 'column'}}> <View style={{ flexDirection: 'column' }}>
<ProfilePictureBannerAndLevel/> <ProfilePictureBannerAndLevel />
<UserMedals/> <UserMedals />
<PlayerStats/> <PlayerStats />
<Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50} zIndex={1}> <Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50} zIndex={1}>
<TextButton <TextButton
onPress={() => navigation.navigate('Settings', { screen: 'Profile' })} onPress={() => navigation.navigate('Settings', { screen: 'Profile' })}
translate={{ translationKey: 'settingsBtn' }} translate={{ translationKey: 'settingsBtn' }}
/> />
</Box> </Box>
</View> </View>
); );
} };
export default ProfileView; export default ProfileView;

View File

@@ -1,23 +1,13 @@
import { import { Card, Column, Image, Row, Text, ScrollView, VStack } from 'native-base';
Card, import Translate from '../components/Translate';
Column, import { RouteProps, useNavigation } from '../Navigation';
Image, import { CardBorderRadius } from '../components/Card';
Row, import TextButton from '../components/TextButton';
Text, import API from '../API';
ScrollView, import CardGridCustom from '../components/CardGridCustom';
VStack, import SongCard from '../components/SongCard';
} from "native-base"; import { useQueries, useQuery } from 'react-query';
import Translate from "../components/Translate"; import { LoadingView } from '../components/Loading';
import SongCardGrid from "../components/SongCardGrid";
import { RouteProps, useNavigation } from "../Navigation";
import { CardBorderRadius } from "../components/Card";
import TextButton from "../components/TextButton";
import API from "../API";
import LoadingComponent from "../components/Loading";
import CardGridCustom from "../components/CardGridCustom";
import SongCard from "../components/SongCard";
import { useQueries, useQuery } from "react-query";
import { LoadingView } from "../components/Loading";
type ScoreViewProps = { type ScoreViewProps = {
songId: number; songId: number;
@@ -35,28 +25,23 @@ type ScoreViewProps = {
}; };
}; };
const ScoreView = ({ const ScoreView = ({ songId, overallScore, precision, score }: RouteProps<ScoreViewProps>) => {
songId,
overallScore,
precision,
score,
}: RouteProps<ScoreViewProps>) => {
const navigation = useNavigation(); const navigation = useNavigation();
const songQuery = useQuery(["song", songId], () => API.getSong(songId)); const songQuery = useQuery(['song', songId], () => API.getSong(songId));
const artistQuery = useQuery( const artistQuery = useQuery(
["song", songId, "artist"], ['song', songId, 'artist'],
() => API.getArtist(songQuery.data!.artistId!), () => API.getArtist(songQuery.data!.artistId!),
{ enabled: songQuery.data != undefined } {
enabled: songQuery.data != undefined,
}
); );
// const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId)); // const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId));
const recommendations = useQuery(["song", "recommendations"], () => const recommendations = useQuery(['song', 'recommendations'], () => API.getSongSuggestions());
API.getSongSuggestions()
);
const artistRecommendations = useQueries( const artistRecommendations = useQueries(
recommendations.data recommendations.data
?.filter(({ artistId }) => artistId !== null) ?.filter(({ artistId }) => artistId !== null)
.map((song) => ({ .map((song) => ({
queryKey: ["artist", song.artistId], queryKey: ['artist', song.artistId],
queryFn: () => API.getArtist(song.artistId!), queryFn: () => API.getArtist(song.artistId!),
})) ?? [] })) ?? []
); );
@@ -71,13 +56,13 @@ const ScoreView = ({
} }
return ( return (
<ScrollView p={8} contentContainerStyle={{ alignItems: "center" }}> <ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
<VStack width={{ base: "100%", lg: "50%" }} textAlign="center"> <VStack width={{ base: '100%', lg: '50%' }} textAlign="center">
<Text bold fontSize="lg"> <Text bold fontSize="lg">
{songQuery.data.name} {songQuery.data.name}
</Text> </Text>
<Text bold>{artistQuery.data?.name}</Text> <Text bold>{artistQuery.data?.name}</Text>
<Row style={{ justifyContent: "center", display: "flex" }}> <Row style={{ justifyContent: 'center', display: 'flex' }}>
<Card shadow={3} style={{ flex: 1 }}> <Card shadow={3} style={{ flex: 1 }}>
<Image <Image
style={{ style={{
@@ -90,7 +75,7 @@ const ScoreView = ({
/> />
</Card> </Card>
<Card shadow={3} style={{ flex: 1 }}> <Card shadow={3} style={{ flex: 1 }}>
<Column style={{ justifyContent: "space-evenly", flexGrow: 1 }}> <Column style={{ justifyContent: 'space-evenly', flexGrow: 1 }}>
{/*<Row style={{ alignItems: 'center' }}> {/*<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'> <Text bold fontSize='xl'>
@@ -103,67 +88,66 @@ const ScoreView = ({
</Text> </Text>
<Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/> <Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/>
</Row>*/} </Row>*/}
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="score" format={(t) => t + " : "} /> <Translate translationKey="score" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{overallScore + "pts"} {overallScore + 'pts'}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="perfect" format={(t) => t + " : "} /> <Translate translationKey="perfect" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{score.perfect} {score.perfect}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="great" format={(t) => t + " : "} /> <Translate translationKey="great" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{score.great} {score.great}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="good" format={(t) => t + " : "} /> <Translate translationKey="good" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{score.good} {score.good}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="wrong" format={(t) => t + " : "} /> <Translate translationKey="wrong" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{score.wrong} {score.wrong}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="missed" format={(t) => t + " : "} /> <Translate translationKey="missed" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{score.missed} {score.missed}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="bestStreak" format={(t) => t + " : "} /> <Translate translationKey="bestStreak" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{score.max_streak} {score.max_streak}
</Text> </Text>
</Row> </Row>
<Row style={{ alignItems: "center" }}> <Row style={{ alignItems: 'center' }}>
<Translate translationKey="precision" format={(t) => t + " : "} /> <Translate translationKey="precision" format={(t) => t + ' : '} />
<Text bold fontSize="xl"> <Text bold fontSize="xl">
{precision + "%"} {precision + '%'}
</Text> </Text>
</Row> </Row>
</Column> </Column>
</Card> </Card>
</Row> </Row>
<CardGridCustom <CardGridCustom
style={{ justifyContent: "space-evenly" }} style={{ justifyContent: 'space-evenly' }}
content={recommendations.data.map((i) => ({ content={recommendations.data.map((i) => ({
cover: i.cover, cover: i.cover,
name: i.name, name: i.name,
artistName: artistName:
artistRecommendations.find(({ data }) => data?.id == i.artistId) artistRecommendations.find(({ data }) => data?.id == i.artistId)?.data
?.data?.name ?? "", ?.name ?? '',
songId: i.id, songId: i.id,
}))} }))}
cardComponent={SongCard} cardComponent={SongCard}
@@ -173,15 +157,15 @@ const ScoreView = ({
</Text> </Text>
} }
/> />
<Row space={3} style={{ width: "100%", justifyContent: "center" }}> <Row space={3} style={{ width: '100%', justifyContent: 'center' }}>
<TextButton <TextButton
colorScheme="gray" colorScheme="gray"
translate={{ translationKey: "backBtn" }} translate={{ translationKey: 'backBtn' }}
onPress={() => navigation.navigate("Home")} onPress={() => navigation.navigate('Home')}
/> />
<TextButton <TextButton
onPress={() => navigation.navigate("Song", { songId })} onPress={() => navigation.navigate('Song', { songId })}
translate={{ translationKey: "playAgain" }} translate={{ translationKey: 'playAgain' }}
/> />
</Row> </Row>
</VStack> </VStack>

View File

@@ -1,19 +1,19 @@
import React, { useState } from "react"; import React, { useState } from 'react';
import SearchBar from "../components/SearchBar"; import SearchBar from '../components/SearchBar';
import Artist from "../models/Artist"; import Artist from '../models/Artist';
import Song from "../models/Song"; import Song from '../models/Song';
import Genre from "../models/Genre"; import Genre from '../models/Genre';
import API from "../API"; import API from '../API';
import { useQuery } from "react-query"; import { useQuery } from 'react-query';
import { SearchResultComponent } from "../components/SearchResult"; import { SearchResultComponent } from '../components/SearchResult';
import { SafeAreaView } from "react-native"; import { SafeAreaView } from 'react-native';
import { Filter } from "../components/SearchBar"; import { Filter } from '../components/SearchBar';
import { ScrollView } from "native-base"; import { ScrollView } from 'native-base';
import { RouteProps } from "../Navigation"; import { RouteProps } from '../Navigation';
interface SearchContextType { interface SearchContextType {
filter: "artist" | "song" | "genre" | "all"; filter: 'artist' | 'song' | 'genre' | 'all';
updateFilter: (newData: "artist" | "song" | "genre" | "all") => void; updateFilter: (newData: 'artist' | 'song' | 'genre' | 'all') => void;
stringQuery: string; stringQuery: string;
updateStringQuery: (newData: string) => void; updateStringQuery: (newData: string) => void;
songData: Song[]; songData: Song[];
@@ -25,9 +25,9 @@ interface SearchContextType {
} }
export const SearchContext = React.createContext<SearchContextType>({ export const SearchContext = React.createContext<SearchContextType>({
filter: "all", filter: 'all',
updateFilter: () => {}, updateFilter: () => {},
stringQuery: "", stringQuery: '',
updateStringQuery: () => {}, updateStringQuery: () => {},
songData: [], songData: [],
artistData: [], artistData: [],
@@ -42,24 +42,23 @@ type SearchViewProps = {
}; };
const SearchView = (props: RouteProps<SearchViewProps>) => { const SearchView = (props: RouteProps<SearchViewProps>) => {
let isRequestSucceeded = false; const [filter, setFilter] = useState<Filter>('all');
const [filter, setFilter] = useState<Filter>("all"); const [stringQuery, setStringQuery] = useState<string>(props?.query ?? '');
const [stringQuery, setStringQuery] = useState<string>(props?.query ?? "");
const { isLoading: isLoadingSong, data: songData = [] } = useQuery( const { isLoading: isLoadingSong, data: songData = [] } = useQuery(
["song", stringQuery], ['song', stringQuery],
() => API.searchSongs(stringQuery), () => API.searchSongs(stringQuery),
{ enabled: !!stringQuery } { enabled: !!stringQuery }
); );
const { isLoading: isLoadingArtist, data: artistData = [] } = useQuery( const { isLoading: isLoadingArtist, data: artistData = [] } = useQuery(
["artist", stringQuery], ['artist', stringQuery],
() => API.searchArtists(stringQuery), () => API.searchArtists(stringQuery),
{ enabled: !!stringQuery } { enabled: !!stringQuery }
); );
const { isLoading: isLoadingGenre, data: genreData = [] } = useQuery( const { isLoading: isLoadingGenre, data: genreData = [] } = useQuery(
["genre", stringQuery], ['genre', stringQuery],
() => API.searchGenres(stringQuery), () => API.searchGenres(stringQuery),
{ enabled: !!stringQuery } { enabled: !!stringQuery }
); );
@@ -72,7 +71,6 @@ const SearchView = (props: RouteProps<SearchViewProps>) => {
const updateStringQuery = (newData: string) => { const updateStringQuery = (newData: string) => {
// called when the stringQuery is updated // called when the stringQuery is updated
setStringQuery(newData); setStringQuery(newData);
isRequestSucceeded = false;
}; };
return ( return (

View File

@@ -6,8 +6,12 @@ import store from '../state/Store';
import AuthenticationView from '../views/AuthenticationView'; import AuthenticationView from '../views/AuthenticationView';
describe('<AuthenticationView />', () => { describe('<AuthenticationView />', () => {
it('has 3 children', () => { it('has 3 children', () => {
const tree = TestRenderer.create(<Provider store={store}><AuthenticationView /></Provider>).toJSON(); const tree = TestRenderer.create(
expect(tree.children.length).toBe(3); <Provider store={store}>
}); <AuthenticationView />
}); </Provider>
).toJSON();
expect(tree.children.length).toBe(3);
});
});

View File

@@ -1,23 +1,13 @@
import { import { Divider, Box, Image, Text, VStack, PresenceTransition, Icon, Stack } from 'native-base';
Divider, import { useQuery } from 'react-query';
Box, import LoadingComponent, { LoadingView } from '../components/Loading';
Center, import React, { useEffect, useState } from 'react';
Image, import { Translate, translate } from '../i18n/i18n';
Text, import formatDuration from 'format-duration';
VStack, import { Ionicons } from '@expo/vector-icons';
PresenceTransition, import API from '../API';
Icon, import TextButton from '../components/TextButton';
Stack, import { RouteProps, useNavigation } from '../Navigation';
} from "native-base";
import { useQuery } from "react-query";
import LoadingComponent, { LoadingView } from "../components/Loading";
import React, { useEffect, useState } from "react";
import { Translate, translate } from "../i18n/i18n";
import formatDuration from "format-duration";
import { Ionicons } from "@expo/vector-icons";
import API from "../API";
import TextButton from "../components/TextButton";
import { RouteProps, useNavigation } from "../Navigation";
interface SongLobbyProps { interface SongLobbyProps {
// The unique identifier to find a song // The unique identifier to find a song
@@ -26,32 +16,30 @@ interface SongLobbyProps {
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => { const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
const navigation = useNavigation(); const navigation = useNavigation();
const songQuery = useQuery(["song", props.songId], () => const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));
API.getSong(props.songId) const chaptersQuery = useQuery(['song', props.songId, 'chapters'], () =>
);
const chaptersQuery = useQuery(["song", props.songId, "chapters"], () =>
API.getSongChapters(props.songId) API.getSongChapters(props.songId)
); );
const scoresQuery = useQuery(["song", props.songId, "scores"], () => const scoresQuery = useQuery(['song', props.songId, 'scores'], () =>
API.getSongHistory(props.songId) API.getSongHistory(props.songId)
); );
const [chaptersOpen, setChaptersOpen] = useState(false); const [chaptersOpen, setChaptersOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (chaptersOpen && !chaptersQuery.data) chaptersQuery.refetch(); if (chaptersOpen && !chaptersQuery.data) chaptersQuery.refetch();
}, [chaptersOpen]); }, [chaptersOpen]);
useEffect(() => { }, [songQuery.isLoading]); useEffect(() => {}, [songQuery.isLoading]);
if (songQuery.isLoading || scoresQuery.isLoading) return <LoadingView />; if (songQuery.isLoading || scoresQuery.isLoading) return <LoadingView />;
return ( return (
<Box style={{ padding: 30, flexDirection: "column" }}> <Box style={{ padding: 30, flexDirection: 'column' }}>
<Box style={{ flexDirection: "row", height: "30%" }}> <Box style={{ flexDirection: 'row', height: '30%' }}>
<Box style={{ flex: 3 }}> <Box style={{ flex: 3 }}>
<Image <Image
source={{ uri: songQuery.data!.cover }} source={{ uri: songQuery.data!.cover }}
alt={songQuery.data?.name} alt={songQuery.data?.name}
style={{ style={{
height: "100%", height: '100%',
width: undefined, width: undefined,
resizeMode: "contain", resizeMode: 'contain',
aspectRatio: 1, aspectRatio: 1,
}} }}
/> />
@@ -61,8 +49,8 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
style={{ style={{
flex: 3, flex: 3,
padding: 10, padding: 10,
flexDirection: "column", flexDirection: 'column',
justifyContent: "space-between", justifyContent: 'space-between',
}} }}
> >
<Stack flex={1} space={3}> <Stack flex={1} space={3}>
@@ -73,30 +61,31 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
<Translate <Translate
translationKey="level" translationKey="level"
format={(level) => format={(level) =>
`${level}: ${chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / `${level}: ${
chaptersQuery.data!.length chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) /
chaptersQuery.data!.length
}` }`
} }
/> />
</Text> </Text>
<TextButton <TextButton
translate={{ translationKey: "playBtn" }} translate={{ translationKey: 'playBtn' }}
width="auto" width="auto"
onPress={() => onPress={() =>
navigation.navigate("Play", { navigation.navigate('Play', {
songId: songQuery.data!.id, songId: songQuery.data!.id,
type: "normal", type: 'normal',
}) })
} }
rightIcon={<Icon as={Ionicons} name="play-outline" />} rightIcon={<Icon as={Ionicons} name="play-outline" />}
/> />
<TextButton <TextButton
translate={{ translationKey: "practiceBtn" }} translate={{ translationKey: 'practiceBtn' }}
width="auto" width="auto"
onPress={() => onPress={() =>
navigation.navigate("Play", { navigation.navigate('Play', {
songId: songQuery.data!.id, songId: songQuery.data!.id,
type: "practice", type: 'practice',
}) })
} }
rightIcon={<Icon as={Ionicons} name="play-outline" />} rightIcon={<Icon as={Ionicons} name="play-outline" />}
@@ -107,18 +96,18 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
</Box> </Box>
<Box <Box
style={{ style={{
flexDirection: "row", flexDirection: 'row',
justifyContent: "space-between", justifyContent: 'space-between',
padding: 30, padding: 30,
}} }}
> >
<Box style={{ flexDirection: "column", alignItems: "center" }}> <Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize="lg"> <Text bold fontSize="lg">
<Translate translationKey="bestScore" /> <Translate translationKey="bestScore" />
</Text> </Text>
<Text>{scoresQuery.data?.best ?? 0}</Text> <Text>{scoresQuery.data?.best ?? 0}</Text>
</Box> </Box>
<Box style={{ flexDirection: "column", alignItems: "center" }}> <Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize="lg"> <Text bold fontSize="lg">
<Translate translationKey="lastScore" /> <Translate translationKey="lastScore" />
</Text> </Text>
@@ -128,15 +117,13 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
{/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */} {/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */}
<Box flexDirection="row"> <Box flexDirection="row">
<TextButton <TextButton
translate={{ translationKey: "chapters" }} translate={{ translationKey: 'chapters' }}
variant="ghost" variant="ghost"
onPress={() => setChaptersOpen(!chaptersOpen)} onPress={() => setChaptersOpen(!chaptersOpen)}
endIcon={ endIcon={
<Icon <Icon
as={Ionicons} as={Ionicons}
name={ name={chaptersOpen ? 'chevron-up-outline' : 'chevron-down-outline'}
chaptersOpen ? "chevron-up-outline" : "chevron-down-outline"
}
/> />
} }
/> />
@@ -154,8 +141,9 @@ const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
> >
<Text>{chapter.name}</Text> <Text>{chapter.name}</Text>
<Text> <Text>
{`${translate("level")} ${chapter.difficulty {`${translate('level')} ${
} - ${formatDuration((chapter.end - chapter.start) * 1000)}`} chapter.difficulty
} - ${formatDuration((chapter.end - chapter.start) * 1000)}`}
</Text> </Text>
</Box> </Box>
))} ))}

View File

@@ -1,5 +1,5 @@
import React from "react"; import React from 'react';
import { useNavigation } from "../Navigation"; import { useNavigation } from '../Navigation';
import { import {
View, View,
Text, Text,
@@ -15,54 +15,52 @@ import {
Row, Row,
Heading, Heading,
Icon, Icon,
} from "native-base"; } from 'native-base';
import { FontAwesome5 } from "@expo/vector-icons"; import { FontAwesome5 } from '@expo/vector-icons';
import BigActionButton from "../components/BigActionButton"; import BigActionButton from '../components/BigActionButton';
import API, { APIError } from "../API"; import API, { APIError } from '../API';
import { setAccessToken } from "../state/UserSlice"; import { setAccessToken } from '../state/UserSlice';
import { useDispatch } from "../state/Store"; import { useDispatch } from '../state/Store';
import { translate } from "../i18n/i18n"; import { translate } from '../i18n/i18n';
const handleGuestLogin = async ( const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promise<string> => {
apiSetter: (accessToken: string) => void
): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount(); const apiAccess = await API.createAndGetGuestAccount();
apiSetter(apiAccess); apiSetter(apiAccess);
return translate("loggedIn"); return translate('loggedIn');
}; };
const imgLogin = const imgLogin =
"https://media.discordapp.net/attachments/717080637038788731/1095980610981478470/Octopus_a_moder_style_image_of_a_musician_showing_a_member_card_c0b9072c-d834-40d5-bc83-796501e1382c.png?width=657&height=657"; 'https://media.discordapp.net/attachments/717080637038788731/1095980610981478470/Octopus_a_moder_style_image_of_a_musician_showing_a_member_card_c0b9072c-d834-40d5-bc83-796501e1382c.png?width=657&height=657';
const imgGuest = const imgGuest =
"https://media.discordapp.net/attachments/717080637038788731/1095996800835539014/Chromacase_guest_2.png?width=865&height=657"; 'https://media.discordapp.net/attachments/717080637038788731/1095996800835539014/Chromacase_guest_2.png?width=865&height=657';
const imgRegister = const imgRegister =
"https://media.discordapp.net/attachments/717080637038788731/1095991220267929641/chromacase_register.png?width=1440&height=511"; 'https://media.discordapp.net/attachments/717080637038788731/1095991220267929641/chromacase_register.png?width=1440&height=511';
const imgBanner = const imgBanner =
"https://chromacase.studio/wp-content/uploads/2023/03/music-sheet-music-color-2462438.jpg"; 'https://chromacase.studio/wp-content/uploads/2023/03/music-sheet-music-color-2462438.jpg';
const imgLogo = const imgLogo =
"https://chromacase.studio/wp-content/uploads/2023/03/cropped-cropped-splashLogo-280x300.png"; 'https://chromacase.studio/wp-content/uploads/2023/03/cropped-cropped-splashLogo-280x300.png';
const StartPageView = () => { const StartPageView = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const screenSize = useBreakpointValue({ base: "small", md: "big" }); const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isSmallScreen = screenSize === "small"; const isSmallScreen = screenSize === 'small';
const dispatch = useDispatch(); const dispatch = useDispatch();
const toast = useToast(); const toast = useToast();
return ( return (
<View <View
style={{ style={{
width: "100%", width: '100%',
height: "100%", height: '100%',
}} }}
> >
<Center> <Center>
<Row <Row
style={{ style={{
alignItems: "center", alignItems: 'center',
justifyContent: "center", justifyContent: 'center',
marginTop: 20, marginTop: 20,
}} }}
> >
@@ -75,17 +73,17 @@ const StartPageView = () => {
}} }}
/> />
} }
size={isSmallScreen ? "5xl" : "6xl"} size={isSmallScreen ? '5xl' : '6xl'}
/> />
<Heading fontSize={isSmallScreen ? "3xl" : "5xl"}>Chromacase</Heading> <Heading fontSize={isSmallScreen ? '3xl' : '5xl'}>Chromacase</Heading>
</Row> </Row>
</Center> </Center>
<Stack <Stack
direction={screenSize === "small" ? "column" : "row"} direction={screenSize === 'small' ? 'column' : 'row'}
style={{ style={{
width: "100%", width: '100%',
justifyContent: "center", justifyContent: 'center',
alignItems: "center", alignItems: 'center',
}} }}
> >
<BigActionButton <BigActionButton
@@ -94,11 +92,11 @@ const StartPageView = () => {
image={imgLogin} image={imgLogin}
iconName="user" iconName="user"
iconProvider={FontAwesome5} iconProvider={FontAwesome5}
onPress={() => navigation.navigate("Login", { isSignup: false })} onPress={() => navigation.navigate('Login', { isSignup: false })}
style={{ style={{
width: isSmallScreen ? "90%" : "clamp(100px, 33.3%, 600px)", width: isSmallScreen ? '90%' : 'clamp(100px, 33.3%, 600px)',
height: "300px", height: '300px',
margin: "clamp(10px, 2%, 50px)", margin: 'clamp(10px, 2%, 50px)',
}} }}
/> />
<BigActionButton <BigActionButton
@@ -121,9 +119,9 @@ const StartPageView = () => {
} }
}} }}
style={{ style={{
width: isSmallScreen ? "90%" : "clamp(100px, 33.3%, 600px)", width: isSmallScreen ? '90%' : 'clamp(100px, 33.3%, 600px)',
height: "300px", height: '300px',
margin: "clamp(10px, 2%, 50px)", margin: 'clamp(10px, 2%, 50px)',
}} }}
/> />
</Stack> </Stack>
@@ -134,60 +132,60 @@ const StartPageView = () => {
subtitle="Create an account to save your progress" subtitle="Create an account to save your progress"
iconProvider={FontAwesome5} iconProvider={FontAwesome5}
iconName="user-plus" iconName="user-plus"
onPress={() => navigation.navigate("Login", { isSignup: true })} onPress={() => navigation.navigate('Login', { isSignup: true })}
style={{ style={{
height: "150px", height: '150px',
width: isSmallScreen ? "90%" : "clamp(150px, 50%, 600px)", width: isSmallScreen ? '90%' : 'clamp(150px, 50%, 600px)',
}} }}
/> />
</Center> </Center>
<Column <Column
style={{ style={{
width: "100%", width: '100%',
marginTop: 40, marginTop: 40,
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Box <Box
style={{ style={{
maxWidth: "90%", maxWidth: '90%',
}} }}
> >
<Heading fontSize="4xl" style={{ textAlign: "center" }}> <Heading fontSize="4xl" style={{ textAlign: 'center' }}>
What is Chromacase? What is Chromacase?
</Heading> </Heading>
<Text fontSize={"xl"}> <Text fontSize={'xl'}>
Chromacase is a free and open source project that aims to provide a Chromacase is a free and open source project that aims to provide a complete
complete learning experience for anyone willing to learn piano. learning experience for anyone willing to learn piano.
</Text> </Text>
</Box> </Box>
<Box <Box
style={{ style={{
width: "90%", width: '90%',
marginTop: 20, marginTop: 20,
}} }}
> >
<Box <Box
style={{ style={{
width: "100%", width: '100%',
height: "100%", height: '100%',
display: "flex", display: 'flex',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Link <Link
href="https://chromacase.studio" href="https://chromacase.studio"
isExternal isExternal
style={{ style={{
width: "clamp(200px, 100%, 700px)", width: 'clamp(200px, 100%, 700px)',
position: "relative", position: 'relative',
overflow: "hidden", overflow: 'hidden',
borderRadius: 10, borderRadius: 10,
}} }}
> >
<AspectRatio ratio={40 / 9} style={{ width: "100%" }}> <AspectRatio ratio={40 / 9} style={{ width: '100%' }}>
<Image <Image
alt="Chromacase Banner" alt="Chromacase Banner"
source={{ uri: imgBanner }} source={{ uri: imgBanner }}
@@ -196,22 +194,22 @@ const StartPageView = () => {
</AspectRatio> </AspectRatio>
<Box <Box
style={{ style={{
position: "absolute", position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
width: "100%", width: '100%',
height: "100%", height: '100%',
backgroundColor: "rgba(0,0,0,0.5)", backgroundColor: 'rgba(0,0,0,0.5)',
}} }}
></Box> ></Box>
<Heading <Heading
fontSize="2xl" fontSize="2xl"
style={{ style={{
textAlign: "center", textAlign: 'center',
position: "absolute", position: 'absolute',
top: "40%", top: '40%',
left: 20, left: 20,
color: "white", color: 'white',
}} }}
> >
Click here for more infos Click here for more infos

View File

@@ -1,31 +1,27 @@
import React from "react"; import React from 'react';
import SignUpForm from "../../components/forms/signupform"; import SignUpForm from '../../components/forms/signupform';
import { Center, Heading, Text } from "native-base"; import { Center, Heading, Text } from 'native-base';
import API, { APIError } from "../../API"; import API, { APIError } from '../../API';
import { translate } from "../../i18n/i18n"; import { translate } from '../../i18n/i18n';
const handleSubmit = async ( const handleSubmit = async (username: string, password: string, email: string) => {
username: string,
password: string,
email: string
) => {
try { try {
await API.transformGuestToUser({ username, password, email }); await API.transformGuestToUser({ username, password, email });
} catch (error) { } catch (error) {
if (error instanceof APIError) return translate(error.userMessage); if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message; if (error instanceof Error) return error.message;
return translate("unknownError"); return translate('unknownError');
} }
return translate("loggedIn"); return translate('loggedIn');
}; };
const GuestToUserView = () => { const GuestToUserView = () => {
return ( return (
<Center flex={1} justifyContent={"center"}> <Center flex={1} justifyContent={'center'}>
<Center width="90%" justifyContent={"center"}> <Center width="90%" justifyContent={'center'}>
<Heading>{translate("signUp")}</Heading> <Heading>{translate('signUp')}</Heading>
<Text mt={5} mb={10}> <Text mt={5} mb={10}>
{translate("transformGuestToUserExplanations")} {translate('transformGuestToUserExplanations')}
</Text> </Text>
<SignUpForm <SignUpForm
onSubmit={(username, password, email) => onSubmit={(username, password, email) =>

View File

@@ -1,72 +1,80 @@
import React from "react"; import React from 'react';
import { Center, Heading } from "native-base"; import { Center, Heading } from 'native-base';
import { translate, Translate } from "../../i18n/i18n"; import { translate, Translate } from '../../i18n/i18n';
import ElementList from "../../components/GtkUI/ElementList"; import ElementList from '../../components/GtkUI/ElementList';
import useUserSettings from "../../hooks/userSettings"; import useUserSettings from '../../hooks/userSettings';
import { LoadingView } from "../../components/Loading"; import { LoadingView } from '../../components/Loading';
const NotificationsView = () => { const NotificationsView = () => {
const { settings, updateSettings } = useUserSettings(); const { settings, updateSettings } = useUserSettings();
if (!settings.data) { if (!settings.data) {
return <LoadingView/> return <LoadingView />;
} }
return ( return (
<Center style={{ flex: 1, justifyContent: "center" }}> <Center style={{ flex: 1, justifyContent: 'center' }}>
<Heading style={{ textAlign: "center" }}> <Heading style={{ textAlign: 'center' }}>
<Translate translationKey="notifBtn" /> <Translate translationKey="notifBtn" />
</Heading> </Heading>
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "toggle", type: 'toggle',
title: translate("SettingsNotificationsPushNotifications"), title: translate('SettingsNotificationsPushNotifications'),
data: { data: {
value: settings.data.notifications.pushNotif, value: settings.data.notifications.pushNotif,
onToggle: () => { onToggle: () => {
updateSettings({ updateSettings({
notifications: { pushNotif: !settings.data.notifications.pushNotif }, notifications: {
pushNotif: !settings.data.notifications.pushNotif,
},
}); });
}, },
}, },
}, },
{ {
type: "toggle", type: 'toggle',
title: translate("SettingsNotificationsEmailNotifications"), title: translate('SettingsNotificationsEmailNotifications'),
data: { data: {
value: settings.data.notifications.emailNotif, value: settings.data.notifications.emailNotif,
onToggle: () => { onToggle: () => {
updateSettings({ updateSettings({
notifications: { emailNotif: !settings.data.notifications.emailNotif }, notifications: {
emailNotif: !settings.data.notifications.emailNotif,
},
}); });
}, },
}, },
}, },
{ {
type: "toggle", type: 'toggle',
title: translate("SettingsNotificationsTrainingReminder"), title: translate('SettingsNotificationsTrainingReminder'),
data: { data: {
value: settings.data.notifications.trainNotif, value: settings.data.notifications.trainNotif,
onToggle: () => { onToggle: () => {
updateSettings({ updateSettings({
notifications: { trainNotif: !settings.data.notifications.trainNotif }, notifications: {
trainNotif: !settings.data.notifications.trainNotif,
},
}); });
}, },
}, },
}, },
{ {
type: "toggle", type: 'toggle',
title: translate("SettingsNotificationsReleaseAlert"), title: translate('SettingsNotificationsReleaseAlert'),
data: { data: {
value: settings.data.notifications.newSongNotif, value: settings.data.notifications.newSongNotif,
onToggle: () => { onToggle: () => {
updateSettings({ updateSettings({
notifications: { newSongNotif: !settings.data.notifications.newSongNotif }, notifications: {
newSongNotif: !settings.data.notifications.newSongNotif,
},
}); });
}, },
}, },

View File

@@ -1,19 +1,12 @@
import React from "react"; import React from 'react';
import { useDispatch } from "react-redux"; import { useDispatch } from 'react-redux';
import { import { Center, Heading } from 'native-base';
Center, import { useLanguage } from '../../state/LanguageSlice';
Heading, import { AvailableLanguages, DefaultLanguage, translate, Translate } from '../../i18n/i18n';
} from "native-base"; import { useSelector } from '../../state/Store';
import { useLanguage } from "../../state/LanguageSlice"; import { updateSettings } from '../../state/SettingsSlice';
import { import ElementList from '../../components/GtkUI/ElementList';
AvailableLanguages, import LocalSettings from '../../models/LocalSettings';
DefaultLanguage,
translate,
Translate,
} from "../../i18n/i18n";
import { useSelector } from "../../state/Store";
import { updateSettings } from "../../state/SettingsSlice";
import ElementList from "../../components/GtkUI/ElementList";
const PreferencesView = () => { const PreferencesView = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -21,37 +14,39 @@ const PreferencesView = () => {
const settings = useSelector((state) => state.settings.local); const settings = useSelector((state) => state.settings.local);
return ( return (
<Center style={{ flex: 1 }}> <Center style={{ flex: 1 }}>
<Heading style={{ textAlign: "center" }}> <Heading style={{ textAlign: 'center' }}>
<Translate translationKey="prefBtn" /> <Translate translationKey="prefBtn" />
</Heading> </Heading>
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "dropdown", type: 'dropdown',
title: translate("SettingsPreferencesTheme"), title: translate('SettingsPreferencesTheme'),
data: { data: {
value: settings.colorScheme, value: settings.colorScheme,
defaultValue: "system", defaultValue: 'system',
onSelect: (newColorScheme) => { onSelect: (newColorScheme) => {
dispatch( dispatch(
updateSettings({ colorScheme: newColorScheme as any }) updateSettings({
colorScheme: newColorScheme as LocalSettings['colorScheme'],
})
); );
}, },
options: [ options: [
{ label: translate("dark"), value: "dark" }, { label: translate('dark'), value: 'dark' },
{ label: translate("light"), value: "light" }, { label: translate('light'), value: 'light' },
{ label: translate("system"), value: "system" }, { label: translate('system'), value: 'system' },
], ],
}, },
}, },
{ {
type: "dropdown", type: 'dropdown',
title: translate("SettingsPreferencesLanguage"), title: translate('SettingsPreferencesLanguage'),
data: { data: {
value: language, value: language,
defaultValue: DefaultLanguage, defaultValue: DefaultLanguage,
@@ -59,25 +54,29 @@ const PreferencesView = () => {
dispatch(useLanguage(itemValue as AvailableLanguages)); dispatch(useLanguage(itemValue as AvailableLanguages));
}, },
options: [ options: [
{ label: "Français", value: "fr" }, { label: 'Français', value: 'fr' },
{ label: "English", value: "en" }, { label: 'English', value: 'en' },
{ label: "Espanol", value: "sp" }, { label: 'Espanol', value: 'sp' },
], ],
}, },
}, },
{ {
type: "dropdown", type: 'dropdown',
title: translate("SettingsPreferencesDifficulty"), title: translate('SettingsPreferencesDifficulty'),
data: { data: {
value: settings.difficulty, value: settings.difficulty,
defaultValue: "medium", defaultValue: 'medium',
onSelect: (itemValue) => { onSelect: (itemValue) => {
dispatch(updateSettings({ difficulty: itemValue as any })); dispatch(
updateSettings({
difficulty: itemValue as LocalSettings['difficulty'],
})
);
}, },
options: [ options: [
{ label: translate("easy"), value: "beg" }, { label: translate('easy'), value: 'beg' },
{ label: translate("medium"), value: "inter" }, { label: translate('medium'), value: 'inter' },
{ label: translate("hard"), value: "pro" }, { label: translate('hard'), value: 'pro' },
], ],
}, },
}, },
@@ -86,13 +85,13 @@ const PreferencesView = () => {
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "toggle", type: 'toggle',
title: translate("SettingsPreferencesColorblindMode"), title: translate('SettingsPreferencesColorblindMode'),
data: { data: {
value: settings.colorBlind, value: settings.colorBlind,
onToggle: () => { onToggle: () => {
@@ -105,13 +104,13 @@ const PreferencesView = () => {
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "range", type: 'range',
title: translate("SettingsPreferencesMicVolume"), title: translate('SettingsPreferencesMicVolume'),
data: { data: {
value: settings.micVolume, value: settings.micVolume,
min: 0, min: 0,

View File

@@ -1,12 +1,12 @@
import React from "react"; import React from 'react';
import { Center, Heading } from "native-base"; import { Center, Heading } from 'native-base';
import { translate } from "../../i18n/i18n"; import { translate } from '../../i18n/i18n';
import ElementList from "../../components/GtkUI/ElementList"; import ElementList from '../../components/GtkUI/ElementList';
import { useDispatch } from "react-redux"; import { useDispatch } from 'react-redux';
import { RootState, useSelector } from "../../state/Store"; import { RootState, useSelector } from '../../state/Store';
import { updateSettings } from "../../state/SettingsSlice"; import { updateSettings } from '../../state/SettingsSlice';
import useUserSettings from "../../hooks/userSettings"; import useUserSettings from '../../hooks/userSettings';
import { LoadingView } from "../../components/Loading"; import { LoadingView } from '../../components/Loading';
const PrivacyView = () => { const PrivacyView = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -14,22 +14,22 @@ const PrivacyView = () => {
const { settings: userSettings, updateSettings: updateUserSettings } = useUserSettings(); const { settings: userSettings, updateSettings: updateUserSettings } = useUserSettings();
if (!userSettings.data) { if (!userSettings.data) {
return <LoadingView/>; return <LoadingView />;
} }
return ( return (
<Center style={{ flex: 1 }}> <Center style={{ flex: 1 }}>
<Heading style={{ textAlign: "center" }}>{translate("privBtn")}</Heading> <Heading style={{ textAlign: 'center' }}>{translate('privBtn')}</Heading>
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "toggle", type: 'toggle',
title: translate("dataCollection"), title: translate('dataCollection'),
data: { data: {
value: settings.dataCollection, value: settings.dataCollection,
onToggle: () => onToggle: () =>
@@ -39,8 +39,8 @@ const PrivacyView = () => {
}, },
}, },
{ {
type: "toggle", type: 'toggle',
title: translate("customAds"), title: translate('customAds'),
data: { data: {
value: settings.customAds, value: settings.customAds,
onToggle: () => onToggle: () =>
@@ -48,12 +48,14 @@ const PrivacyView = () => {
}, },
}, },
{ {
type: "toggle", type: 'toggle',
title: translate("recommendations"), title: translate('recommendations'),
data: { data: {
value: userSettings.data.recommendations, value: userSettings.data.recommendations,
onToggle: () => onToggle: () =>
updateUserSettings({ recommendations: !userSettings.data.recommendations }) updateUserSettings({
recommendations: !userSettings.data.recommendations,
}),
}, },
}, },
]} ]}

View File

@@ -1,48 +1,43 @@
import API from "../../API"; import API from '../../API';
import { useDispatch } from "react-redux"; import { useDispatch } from 'react-redux';
import { unsetAccessToken } from "../../state/UserSlice"; import { unsetAccessToken } from '../../state/UserSlice';
import React from "react"; import React from 'react';
import { import { Column, Text, Button, Box, Flex, Center, Heading, Avatar, Popover } from 'native-base';
Column, import TextButton from '../../components/TextButton';
Text, import { LoadingView } from '../../components/Loading';
Button, import ElementList from '../../components/GtkUI/ElementList';
Box, import { translate } from '../../i18n/i18n';
Flex, import { useQuery } from 'react-query';
Center,
Heading,
Avatar,
Popover,
} from "native-base";
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";
const getInitials = (name: string) => { const getInitials = (name: string) => {
return name.split(" ").map((n) => n[0]).join(""); return name
.split(' ')
.map((n) => n[0])
.join('');
}; };
// 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 ProfileSettings = ({ navigation }: { navigation: any }) => {
const userQuery = useQuery(['user'], () => API.getUserInfo()); const userQuery = useQuery(['user'], () => API.getUserInfo());
const dispatch = useDispatch(); const dispatch = useDispatch();
if (!userQuery.data || userQuery.isLoading) { if (!userQuery.data || userQuery.isLoading) {
return <LoadingView/> return <LoadingView />;
} }
const user = userQuery.data; const user = userQuery.data;
return ( return (
<Flex <Flex
style={{ style={{
flex: 1, flex: 1,
alignItems: "center", alignItems: 'center',
paddingTop: 40, paddingTop: 40,
}} }}
> >
<Column <Column
style={{ style={{
width: "100%", width: '100%',
alignItems: "center", alignItems: 'center',
}} }}
> >
<Center> <Center>
@@ -53,17 +48,17 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "text", type: 'text',
title: translate("email"), title: translate('email'),
data: { data: {
text: user.email || translate("NoAssociatedEmail"), text: user.email || translate('NoAssociatedEmail'),
onPress: () => { onPress: () => {
navigation.navigate("ChangeEmail"); navigation.navigate('ChangeEmail');
}, },
}, },
}, },
@@ -73,54 +68,54 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<ElementList <ElementList
style={{ style={{
marginTop: 20, marginTop: 20,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "text", type: 'text',
title: translate("username"), title: translate('username'),
data: { data: {
text: user.name, text: user.name,
}, },
}, },
{ {
type: "text", type: 'text',
title: "ID", title: 'ID',
helperText: "This is your unique ID, be proud of it!", helperText: 'This is your unique ID, be proud of it!',
data: { data: {
text: user.id.toString(), text: user.id.toString(),
}, },
}, },
{ {
type: "text", type: 'text',
title: translate("nbGamesPlayed"), title: translate('nbGamesPlayed'),
data: { data: {
text: user.data.gamesPlayed.toString(), text: user.data.gamesPlayed.toString(),
}, },
}, },
{ {
type: "text", type: 'text',
title: "XP", title: 'XP',
description: translate("XPDescription"), description: translate('XPDescription'),
data: { data: {
text: user.data.xp.toString(), text: user.data.xp.toString(),
}, },
}, },
{ {
type: "text", type: 'text',
title: translate("userCreatedAt"), title: translate('userCreatedAt'),
helperText: helperText:
"La date de création est actuellement arbitraire car le serveur ne retourne pas cette information", 'La date de création est actuellement arbitraire car le serveur ne retourne pas cette information',
data: { data: {
text: user.data.createdAt.toLocaleDateString(), text: user.data.createdAt.toLocaleDateString(),
}, },
}, },
{ {
type: "text", type: 'text',
title: translate("premiumAccount"), title: translate('premiumAccount'),
data: { data: {
text: translate(user.premium ? "yes" : "no"), text: translate(user.premium ? 'yes' : 'no'),
}, },
}, },
]} ]}
@@ -131,17 +126,17 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<ElementList <ElementList
style={{ style={{
marginTop: 10, marginTop: 10,
width: "90%", width: '90%',
maxWidth: 850, maxWidth: 850,
}} }}
elements={[ elements={[
{ {
type: "toggle", type: 'toggle',
title: "Piano Magique", title: 'Piano Magique',
description: description:
"Fait apparaître de la lumière sur le piano pendant les parties", 'Fait apparaître de la lumière sur le piano pendant les parties',
helperText: helperText:
"Vous devez posséder le module physique lumineux Chromacase pour pouvoir utiliser cette fonctionnalité", 'Vous devez posséder le module physique lumineux Chromacase pour pouvoir utiliser cette fonctionnalité',
disabled: true, disabled: true,
data: { data: {
value: false, value: false,
@@ -149,20 +144,20 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
}, },
}, },
{ {
type: "dropdown", type: 'dropdown',
title: "Thème de piano", title: 'Thème de piano',
disabled: true, disabled: true,
data: { data: {
value: "default", value: 'default',
onSelect: () => {}, onSelect: () => {},
options: [ options: [
{ {
label: "Default", label: 'Default',
value: "default", value: 'default',
}, },
{ {
label: "Catpuccino", label: 'Catpuccino',
value: "catpuccino", value: 'catpuccino',
}, },
], ],
}, },
@@ -176,41 +171,39 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<TextButton <TextButton
onPress={() => dispatch(unsetAccessToken())} onPress={() => dispatch(unsetAccessToken())}
translate={{ translate={{
translationKey: "signOutBtn", translationKey: 'signOutBtn',
}} }}
/> />
)} )}
{user.isGuest && ( {user.isGuest && (
<Popover <Popover
trigger={(triggerProps) => ( trigger={(triggerProps) => (
<Button {...triggerProps}>{translate("signOutBtn")}</Button> <Button {...triggerProps}>{translate('signOutBtn')}</Button>
)} )}
> >
<Popover.Content> <Popover.Content>
<Popover.Arrow /> <Popover.Arrow />
<Popover.Body> <Popover.Body>
<Heading size="md" mb={2}> <Heading size="md" mb={2}>
{translate("Attention")} {translate('Attention')}
</Heading> </Heading>
<Text> <Text>
{translate( {translate('YouAreCurrentlyConnectedWithAGuestAccountWarning')}
"YouAreCurrentlyConnectedWithAGuestAccountWarning"
)}
</Text> </Text>
<Button.Group variant="ghost" space={2}> <Button.Group variant="ghost" space={2}>
<Button <Button
onPress={() => dispatch(unsetAccessToken())} onPress={() => dispatch(unsetAccessToken())}
colorScheme="red" colorScheme="red"
> >
{translate("signOutBtn")} {translate('signOutBtn')}
</Button> </Button>
<Button <Button
onPress={() => { onPress={() => {
navigation.navigate("GuestToUser"); navigation.navigate('GuestToUser');
}} }}
colorScheme="green" colorScheme="green"
> >
{translate("signUpBtn")} {translate('signUpBtn')}
</Button> </Button>
</Button.Group> </Button.Group>
</Popover.Body> </Popover.Body>

View File

@@ -1,9 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Center, Button, Text, Heading, Box } from "native-base"; import { Center, Text, Heading, Box } from 'native-base';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { translate } from '../../i18n/i18n';
import { unsetAccessToken } from '../../state/UserSlice';
import { useDispatch } from "react-redux";
import { translate, Translate } from "../../i18n/i18n";
import createTabRowNavigator from '../../components/navigators/TabRowNavigator'; import createTabRowNavigator from '../../components/navigators/TabRowNavigator';
import { MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import ChangePasswordForm from '../../components/forms/changePasswordForm'; import ChangePasswordForm from '../../components/forms/changePasswordForm';
@@ -17,69 +14,66 @@ import { useQuery } from 'react-query';
import API from '../../API'; import API from '../../API';
const handleChangeEmail = async (newEmail: string): Promise<string> => { const handleChangeEmail = async (newEmail: string): Promise<string> => {
try { await API.updateUserEmail(newEmail);
let response = await API.updateUserEmail(newEmail); return translate('emailUpdated');
return translate('emailUpdated'); };
} catch (e) {
throw e;
}
}
const handleChangePassword = async (oldPassword: string, newPassword: string): Promise<string> => { const handleChangePassword = async (oldPassword: string, newPassword: string): Promise<string> => {
try { await API.updateUserPassword(oldPassword, newPassword);
let response = await API.updateUserPassword(oldPassword, newPassword); return translate('passwordUpdated');
return translate('passwordUpdated'); };
} catch (e) {
throw e;
}
}
export const ChangePasswordView = () => { export const ChangePasswordView = () => {
return ( return (
<Center style={{ flex: 1}}> <Center style={{ flex: 1 }}>
<Heading paddingBottom={'2%'}>{translate('changePassword')}</Heading> <Heading paddingBottom={'2%'}>{translate('changePassword')}</Heading>
<ChangePasswordForm onSubmit={(oldPassword, newPassword) => handleChangePassword(oldPassword, newPassword)}/> <ChangePasswordForm
onSubmit={(oldPassword, newPassword) =>
handleChangePassword(oldPassword, newPassword)
}
/>
</Center> </Center>
) );
} };
export const ChangeEmailView = () => { export const ChangeEmailView = () => {
return ( return (
<Center style={{ flex: 1}}> <Center style={{ flex: 1 }}>
<Heading paddingBottom={'2%'}>{translate('changeEmail')}</Heading> <Heading paddingBottom={'2%'}>{translate('changeEmail')}</Heading>
<ChangeEmailForm onSubmit={(oldEmail, newEmail) => handleChangeEmail(newEmail)}/> <ChangeEmailForm onSubmit={(oldEmail, newEmail) => handleChangeEmail(newEmail)} />
</Center> </Center>
) );
} };
export const GoogleAccountView = () => { export const GoogleAccountView = () => {
return ( return (
<Center style={{ flex: 1}}> <Center style={{ flex: 1 }}>
<Text>GoogleAccount</Text> <Text>GoogleAccount</Text>
</Center> </Center>
) );
} };
export const PianoSettingsView = () => { export const PianoSettingsView = () => {
return ( return (
<Center style={{ flex: 1}}> <Center style={{ flex: 1 }}>
<Text>Global settings for the virtual piano</Text> <Text>Global settings for the virtual piano</Text>
</Center> </Center>
) );
} };
const TabRow = createTabRowNavigator(); const TabRow = createTabRowNavigator();
type SetttingsNavigatorProps = { type SetttingsNavigatorProps = {
screen?: 'Profile' | screen?:
'Preferences' | | 'Profile'
'Notifications' | | 'Preferences'
'Privacy' | | 'Notifications'
'ChangePassword' | | 'Privacy'
'ChangeEmail' | | 'ChangePassword'
'GoogleAccount' | | 'ChangeEmail'
'PianoSettings' | 'GoogleAccount'
} | 'PianoSettings';
};
const SetttingsNavigator = (props?: SetttingsNavigatorProps) => { const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
const userQuery = useQuery(['user'], () => API.getUserInfo()); const userQuery = useQuery(['user'], () => API.getUserInfo());
@@ -87,66 +81,106 @@ const SetttingsNavigator = (props?: SetttingsNavigatorProps) => {
if (userQuery.isLoading) { if (userQuery.isLoading) {
return ( return (
<Center style={{ flex: 1}}> <Center style={{ flex: 1 }}>
<Text>Loading...</Text> <Text>Loading...</Text>
</Center> </Center>
) );
} }
return ( return (
<TabRow.Navigator initialRouteName={props?.screen ?? 'InternalDefault'} contentStyle={{}} tabBarStyle={{}}> <TabRow.Navigator
initialRouteName={props?.screen ?? 'InternalDefault'}
contentStyle={{}}
tabBarStyle={{}}
>
{/* I'm doing this to be able to land on the summary of settings when clicking on settings and directly to the {/* I'm doing this to be able to land on the summary of settings when clicking on settings and directly to the
wanted settings page if needed so I need to do special work with the 0 index */} wanted settings page if needed so I need to do special work with the 0 index */}
<TabRow.Screen name='InternalDefault' component={Box} /> <TabRow.Screen name="InternalDefault" component={Box} />
{user && user.isGuest && {user && user.isGuest && (
<TabRow.Screen name='GuestToUser' component={GuestToUserView} options={{ <TabRow.Screen
title: translate('SettingsCategoryGuest'), name="GuestToUser"
component={GuestToUserView}
options={{
title: translate('SettingsCategoryGuest'),
iconProvider: FontAwesome5,
iconName: 'user-clock',
}}
/>
)}
<TabRow.Screen
name="Profile"
component={ProfileSettings}
options={{
title: translate('SettingsCategoryProfile'),
iconProvider: FontAwesome5, iconProvider: FontAwesome5,
iconName: "user-clock" iconName: 'user',
}} /> }}
} />
<TabRow.Screen name='Profile' component={ProfileSettings} options={{ <TabRow.Screen
title: translate('SettingsCategoryProfile'), name="Preferences"
iconProvider: FontAwesome5, component={PreferencesView}
iconName: "user" options={{
}} /> title: translate('SettingsCategoryPreferences'),
<TabRow.Screen name='Preferences' component={PreferencesView} options={{ iconProvider: FontAwesome5,
title: translate('SettingsCategoryPreferences'), iconName: 'music',
iconProvider: FontAwesome5, }}
iconName: "music" />
}} /> <TabRow.Screen
<TabRow.Screen name='Notifications' component={NotificationsView} options={{ name="Notifications"
title: translate('SettingsCategoryNotifications'), component={NotificationsView}
iconProvider: FontAwesome5, options={{
iconName: "bell" title: translate('SettingsCategoryNotifications'),
}}/> iconProvider: FontAwesome5,
<TabRow.Screen name='Privacy' component={PrivacyView} options={{ iconName: 'bell',
title: translate('SettingsCategoryPrivacy'), }}
iconProvider: FontAwesome5, />
iconName: "lock" <TabRow.Screen
}} /> name="Privacy"
<TabRow.Screen name='ChangePassword' component={ChangePasswordView} options={{ component={PrivacyView}
title: translate('SettingsCategorySecurity'), options={{
iconProvider: FontAwesome5, title: translate('SettingsCategoryPrivacy'),
iconName: "key" iconProvider: FontAwesome5,
}}/> iconName: 'lock',
<TabRow.Screen name='ChangeEmail' component={ChangeEmailView} options={{ }}
title: translate('SettingsCategoryEmail'), />
iconProvider: FontAwesome5, <TabRow.Screen
iconName: "envelope" name="ChangePassword"
}} /> component={ChangePasswordView}
<TabRow.Screen name='GoogleAccount' component={GoogleAccountView} options={{ options={{
title: translate('SettingsCategoryGoogle'), title: translate('SettingsCategorySecurity'),
iconProvider: FontAwesome5, iconProvider: FontAwesome5,
iconName: "google" iconName: 'key',
}} /> }}
<TabRow.Screen name='PianoSettings' component={PianoSettingsView} options={{ />
title: translate('SettingsCategoryPiano'), <TabRow.Screen
iconProvider: MaterialCommunityIcons, name="ChangeEmail"
iconName: "piano" component={ChangeEmailView}
}} /> options={{
title: translate('SettingsCategoryEmail'),
iconProvider: FontAwesome5,
iconName: 'envelope',
}}
/>
<TabRow.Screen
name="GoogleAccount"
component={GoogleAccountView}
options={{
title: translate('SettingsCategoryGoogle'),
iconProvider: FontAwesome5,
iconName: 'google',
}}
/>
<TabRow.Screen
name="PianoSettings"
component={PianoSettingsView}
options={{
title: translate('SettingsCategoryPiano'),
iconProvider: MaterialCommunityIcons,
iconName: 'piano',
}}
/>
</TabRow.Navigator> </TabRow.Navigator>
) );
} };
export default SetttingsNavigator; export default SetttingsNavigator;

View File

@@ -1,17 +1,15 @@
const createExpoWebpackConfigAsync = require('@expo/webpack-config') const createExpoWebpackConfigAsync = require('@expo/webpack-config');
module.exports = async function (env, argv) { module.exports = async function (env, argv) {
const config = await createExpoWebpackConfigAsync( const config = await createExpoWebpackConfigAsync(
{ {
...env, ...env,
babel: { dangerouslyAddModulePathsToTranspile: ['moti'] }, babel: { dangerouslyAddModulePathsToTranspile: ['moti'] },
}, },
argv argv
) );
config.resolve.alias['framer-motion'] = 'framer-motion/dist/framer-motion' config.resolve.alias['framer-motion'] = 'framer-motion/dist/framer-motion';
return config;
};
return config
}

View File

@@ -1415,6 +1415,38 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==
dependencies:
eslint-visitor-keys "^3.3.0"
"@eslint-community/regexpp@^4.4.0":
version "4.5.1"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884"
integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==
"@eslint/eslintrc@^2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.0.3.tgz#4910db5505f4d503f27774bf356e3704818a0331"
integrity sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==
dependencies:
ajv "^6.12.4"
debug "^4.3.2"
espree "^9.5.2"
globals "^13.19.0"
ignore "^5.2.0"
import-fresh "^3.2.1"
js-yaml "^4.1.0"
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@eslint/js@8.42.0":
version "8.42.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.42.0.tgz#484a1d638de2911e6f5a30c12f49c7e4a3270fb6"
integrity sha512-6SWlXpWU5AvId8Ac7zjzmIOqMOba/JWY8XZ4A7q7Gn1Vlfg/SFFIlrtHXt9nPn4op9ZPAkl91Jao+QQv3r/ukw==
"@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0": "@expo/bunyan@4.0.0", "@expo/bunyan@^4.0.0":
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/@expo/bunyan/-/bunyan-4.0.0.tgz#be0c1de943c7987a9fbd309ea0b1acd605890c7b" resolved "https://registry.yarnpkg.com/@expo/bunyan/-/bunyan-4.0.0.tgz#be0c1de943c7987a9fbd309ea0b1acd605890c7b"
@@ -1899,6 +1931,25 @@
dependencies: dependencies:
"@hapi/hoek" "^9.0.0" "@hapi/hoek" "^9.0.0"
"@humanwhocodes/config-array@^0.11.10":
version "0.11.10"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2"
integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==
dependencies:
"@humanwhocodes/object-schema" "^1.2.1"
debug "^4.1.1"
minimatch "^3.0.5"
"@humanwhocodes/module-importer@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c"
integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
"@humanwhocodes/object-schema@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@internationalized/date@^3.0.2": "@internationalized/date@^3.0.2":
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.0.2.tgz#1566a0bcbd82dce4dd54a5b26456bb701068cb89" resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.0.2.tgz#1566a0bcbd82dce4dd54a5b26456bb701068cb89"
@@ -2384,7 +2435,7 @@
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b"
integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==
"@nodelib/fs.walk@^1.2.3": "@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8":
version "1.2.8" version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
@@ -4680,6 +4731,11 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/json-schema@^7.0.9":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
"@types/keyv@^3.1.4": "@types/keyv@^3.1.4":
version "3.1.4" version "3.1.4"
resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6"
@@ -4833,6 +4889,11 @@
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
"@types/semver@^7.3.12":
version "7.5.0"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a"
integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==
"@types/source-list-map@*": "@types/source-list-map@*":
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@@ -4927,6 +4988,90 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^5.43.0":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.11.tgz#8d466aa21abea4c3f37129997b198d141f09e76f"
integrity sha512-XxuOfTkCUiOSyBWIvHlUraLw/JT/6Io1365RO6ZuI88STKMavJZPNMU0lFcUTeQXEhHiv64CbxYxBNoDVSmghg==
dependencies:
"@eslint-community/regexpp" "^4.4.0"
"@typescript-eslint/scope-manager" "5.59.11"
"@typescript-eslint/type-utils" "5.59.11"
"@typescript-eslint/utils" "5.59.11"
debug "^4.3.4"
grapheme-splitter "^1.0.4"
ignore "^5.2.0"
natural-compare-lite "^1.4.0"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/parser@^5.0.0":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.11.tgz#af7d4b7110e3068ce0b97550736de455e4250103"
integrity sha512-s9ZF3M+Nym6CAZEkJJeO2TFHHDsKAM3ecNkLuH4i4s8/RCPnF5JRip2GyviYkeEAcwGMJxkqG9h2dAsnA1nZpA==
dependencies:
"@typescript-eslint/scope-manager" "5.59.11"
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/typescript-estree" "5.59.11"
debug "^4.3.4"
"@typescript-eslint/scope-manager@5.59.11":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.11.tgz#5d131a67a19189c42598af9fb2ea1165252001ce"
integrity sha512-dHFOsxoLFtrIcSj5h0QoBT/89hxQONwmn3FOQ0GOQcLOOXm+MIrS8zEAhs4tWl5MraxCY3ZJpaXQQdFMc2Tu+Q==
dependencies:
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/visitor-keys" "5.59.11"
"@typescript-eslint/type-utils@5.59.11":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.11.tgz#5eb67121808a84cb57d65a15f48f5bdda25f2346"
integrity sha512-LZqVY8hMiVRF2a7/swmkStMYSoXMFlzL6sXV6U/2gL5cwnLWQgLEG8tjWPpaE4rMIdZ6VKWwcffPlo1jPfk43g==
dependencies:
"@typescript-eslint/typescript-estree" "5.59.11"
"@typescript-eslint/utils" "5.59.11"
debug "^4.3.4"
tsutils "^3.21.0"
"@typescript-eslint/types@5.59.11":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.11.tgz#1a9018fe3c565ba6969561f2a49f330cf1fe8db1"
integrity sha512-epoN6R6tkvBYSc+cllrz+c2sOFWkbisJZWkOE+y3xHtvYaOE6Wk6B8e114McRJwFRjGvYdJwLXQH5c9osME/AA==
"@typescript-eslint/typescript-estree@5.59.11":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.11.tgz#b2caaa31725e17c33970c1197bcd54e3c5f42b9f"
integrity sha512-YupOpot5hJO0maupJXixi6l5ETdrITxeo5eBOeuV7RSKgYdU3G5cxO49/9WRnJq9EMrB7AuTSLH/bqOsXi7wPA==
dependencies:
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/visitor-keys" "5.59.11"
debug "^4.3.4"
globby "^11.1.0"
is-glob "^4.0.3"
semver "^7.3.7"
tsutils "^3.21.0"
"@typescript-eslint/utils@5.59.11":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.11.tgz#9dbff49dc80bfdd9289f9f33548f2e8db3c59ba1"
integrity sha512-didu2rHSOMUdJThLk4aZ1Or8IcO3HzCw/ZvEjTTIfjIrcdd5cvSIwwDy2AOlE7htSNp7QIZ10fLMyRCveesMLg==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@types/json-schema" "^7.0.9"
"@types/semver" "^7.3.12"
"@typescript-eslint/scope-manager" "5.59.11"
"@typescript-eslint/types" "5.59.11"
"@typescript-eslint/typescript-estree" "5.59.11"
eslint-scope "^5.1.1"
semver "^7.3.7"
"@typescript-eslint/visitor-keys@5.59.11":
version "5.59.11"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.11.tgz#dca561ddad169dc27d62396d64f45b2d2c3ecc56"
integrity sha512-KGYniTGG3AMTuKF9QBD7EIrvufkB6O6uX3knP73xbKLMpH+QRPcgnCxjWXSHjMRuOxFLovljqQgQpR0c7GvjoA==
dependencies:
"@typescript-eslint/types" "5.59.11"
eslint-visitor-keys "^3.3.0"
"@urql/core@2.3.6": "@urql/core@2.3.6":
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552" resolved "https://registry.yarnpkg.com/@urql/core/-/core-2.3.6.tgz#ee0a6f8fde02251e9560c5f17dce5cd90f948552"
@@ -5371,7 +5516,7 @@ acorn-import-assertions@^1.7.6:
resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9"
integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
acorn-jsx@^5.3.1: acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
version "5.3.2" version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
@@ -5391,7 +5536,7 @@ acorn@^7.1.1, acorn@^7.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1: acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0:
version "8.8.2" version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
@@ -5473,7 +5618,7 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5:
version "6.12.6" version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -5701,7 +5846,7 @@ array-flatten@^2.1.0:
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
array-includes@^3.0.3: array-includes@^3.0.3, array-includes@^3.1.5, array-includes@^3.1.6:
version "3.1.6" version "3.1.6"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
@@ -5744,7 +5889,7 @@ array.prototype.flat@^1.2.1:
es-abstract "^1.20.4" es-abstract "^1.20.4"
es-shim-unscopables "^1.0.0" es-shim-unscopables "^1.0.0"
array.prototype.flatmap@^1.2.1: array.prototype.flatmap@^1.2.1, array.prototype.flatmap@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
@@ -5776,6 +5921,17 @@ array.prototype.reduce@^1.0.5:
es-array-method-boxes-properly "^1.0.0" es-array-method-boxes-properly "^1.0.0"
is-string "^1.0.7" is-string "^1.0.7"
array.prototype.tosorted@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532"
integrity sha512-pZYPXPRl2PqWcsUs6LOMn+1f1532nEoPTYowBtqLwAW+W8vSVhkIGnmOX1t/UQjD6YGI0vcD2B1U7ZFGQH9jnQ==
dependencies:
call-bind "^1.0.2"
define-properties "^1.1.4"
es-abstract "^1.20.4"
es-shim-unscopables "^1.0.0"
get-intrinsic "^1.1.3"
arrify@^2.0.1: arrify@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
@@ -7435,7 +7591,7 @@ cross-fetch@^3.1.5:
dependencies: dependencies:
node-fetch "2.6.7" node-fetch "2.6.7"
cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.3: cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@@ -7810,7 +7966,7 @@ deep-extend@^0.6.0:
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
deep-is@~0.1.3: deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
@@ -8065,6 +8221,13 @@ dns-txt@^2.0.2:
dependencies: dependencies:
buffer-indexof "^1.0.0" buffer-indexof "^1.0.0"
doctrine@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d"
integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==
dependencies:
esutils "^2.0.2"
doctrine@^3.0.0: doctrine@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -8572,7 +8735,40 @@ escodegen@^2.0.0:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-scope@5.1.1: eslint-config-prettier@^8.3.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
eslint-plugin-prettier@^4.0.0:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-react@^7.31.11:
version "7.32.2"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.32.2.tgz#e71f21c7c265ebce01bcbc9d0955170c55571f10"
integrity sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==
dependencies:
array-includes "^3.1.6"
array.prototype.flatmap "^1.3.1"
array.prototype.tosorted "^1.1.1"
doctrine "^2.1.0"
estraverse "^5.3.0"
jsx-ast-utils "^2.4.1 || ^3.0.0"
minimatch "^3.1.2"
object.entries "^1.1.6"
object.fromentries "^2.0.6"
object.hasown "^1.1.2"
object.values "^1.1.6"
prop-types "^15.8.1"
resolve "^2.0.0-next.4"
semver "^6.3.0"
string.prototype.matchall "^4.0.8"
eslint-scope@5.1.1, eslint-scope@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -8588,11 +8784,85 @@ eslint-scope@^4.0.3:
esrecurse "^4.1.0" esrecurse "^4.1.0"
estraverse "^4.1.1" estraverse "^4.1.1"
eslint-scope@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
dependencies:
esrecurse "^4.3.0"
estraverse "^5.2.0"
eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
version "3.4.1"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint@^8.42.0:
version "8.42.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.42.0.tgz#7bebdc3a55f9ed7167251fe7259f75219cade291"
integrity sha512-ulg9Ms6E1WPf67PHaEY4/6E2tEn5/f7FXGzr3t9cBMugOmf1INYvuUwwh1aXQN4MfJ6a5K2iNwP3w4AColvI9A==
dependencies:
"@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.4.0"
"@eslint/eslintrc" "^2.0.3"
"@eslint/js" "8.42.0"
"@humanwhocodes/config-array" "^0.11.10"
"@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.3.2"
doctrine "^3.0.0"
escape-string-regexp "^4.0.0"
eslint-scope "^7.2.0"
eslint-visitor-keys "^3.4.1"
espree "^9.5.2"
esquery "^1.4.2"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
find-up "^5.0.0"
glob-parent "^6.0.2"
globals "^13.19.0"
graphemer "^1.4.0"
ignore "^5.2.0"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
is-path-inside "^3.0.3"
js-yaml "^4.1.0"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.1.2"
natural-compare "^1.4.0"
optionator "^0.9.1"
strip-ansi "^6.0.1"
strip-json-comments "^3.1.0"
text-table "^0.2.0"
espree@^9.5.2:
version "9.5.2"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b"
integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==
dependencies:
acorn "^8.8.0"
acorn-jsx "^5.3.2"
eslint-visitor-keys "^3.4.1"
esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0:
version "4.0.1" version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.2:
version "1.5.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b"
integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==
dependencies:
estraverse "^5.1.0"
esrecurse@^4.1.0, esrecurse@^4.3.0: esrecurse@^4.1.0, esrecurse@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
@@ -8605,7 +8875,7 @@ estraverse@^4.1.1:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.2.0: estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
@@ -9037,6 +9307,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.3.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
fast-glob@^2.2.6: fast-glob@^2.2.6:
version "2.2.7" version "2.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d"
@@ -9070,7 +9345,7 @@ fast-json-stable-stringify@^2.0.0:
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-levenshtein@~2.0.6: fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
@@ -9148,6 +9423,13 @@ figgy-pudding@^3.5.1:
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
dependencies:
flat-cache "^3.0.4"
file-loader@^6.2.0: file-loader@^6.2.0:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d"
@@ -9728,6 +10010,13 @@ glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2:
dependencies: dependencies:
is-glob "^4.0.1" is-glob "^4.0.1"
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
dependencies:
is-glob "^4.0.3"
glob-promise@^3.4.0: glob-promise@^3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20" resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-3.4.0.tgz#b6b8f084504216f702dc2ce8c9bc9ac8866fdb20"
@@ -9820,6 +10109,13 @@ globals@^11.1.0:
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
globals@^13.19.0:
version "13.20.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82"
integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==
dependencies:
type-fest "^0.20.2"
globalthis@^1.0.0, globalthis@^1.0.3: globalthis@^1.0.0, globalthis@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
@@ -9839,7 +10135,7 @@ globby@11.0.1:
merge2 "^1.3.0" merge2 "^1.3.0"
slash "^3.0.0" slash "^3.0.0"
globby@^11.0.1, globby@^11.0.2: globby@^11.0.1, globby@^11.0.2, globby@^11.1.0:
version "11.1.0" version "11.1.0"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==
@@ -9922,6 +10218,11 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
graphql-tag@^2.10.1: graphql-tag@^2.10.1:
version "2.12.6" version "2.12.6"
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
@@ -10542,7 +10843,7 @@ import-fresh@^2.0.0:
caller-path "^2.0.0" caller-path "^2.0.0"
resolve-from "^3.0.0" resolve-from "^3.0.0"
import-fresh@^3.1.0, import-fresh@^3.2.1: import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -10973,7 +11274,7 @@ is-glob@^3.0.0, is-glob@^3.1.0:
dependencies: dependencies:
is-extglob "^2.1.0" is-extglob "^2.1.0"
is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -11060,7 +11361,7 @@ is-path-inside@^2.1.0:
dependencies: dependencies:
path-is-inside "^1.0.2" path-is-inside "^1.0.2"
is-path-inside@^3.0.2: is-path-inside@^3.0.2, is-path-inside@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
@@ -12042,6 +12343,11 @@ json-schema-traverse@^0.4.1:
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
json3@^3.3.2: json3@^3.3.2:
version "3.3.3" version "3.3.3"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
@@ -12087,6 +12393,14 @@ jsonfile@^6.0.1:
optionalDependencies: optionalDependencies:
graceful-fs "^4.1.6" graceful-fs "^4.1.6"
"jsx-ast-utils@^2.4.1 || ^3.0.0":
version "3.3.3"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.3.tgz#76b3e6e6cece5c69d49a5792c3d01bd1a0cdc7ea"
integrity sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==
dependencies:
array-includes "^3.1.5"
object.assign "^4.1.3"
jszip@3.10.1: jszip@3.10.1:
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2" resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
@@ -12179,6 +12493,14 @@ leven@^3.1.0:
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
dependencies:
prelude-ls "^1.2.1"
type-check "~0.4.0"
levn@~0.3.0: levn@~0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee"
@@ -13161,7 +13483,7 @@ minimalistic-crypto-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==
"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1: "minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2" version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -13455,6 +13777,11 @@ native-base@^3.4.17:
tinycolor2 "^1.4.2" tinycolor2 "^1.4.2"
use-subscription "^1.8.0" use-subscription "^1.8.0"
natural-compare-lite@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4"
integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -13814,7 +14141,7 @@ object-visit@^1.0.0:
dependencies: dependencies:
isobject "^3.0.0" isobject "^3.0.0"
object.assign@^4.1.0, object.assign@^4.1.4: object.assign@^4.1.0, object.assign@^4.1.3, object.assign@^4.1.4:
version "4.1.4" version "4.1.4"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==
@@ -13824,7 +14151,7 @@ object.assign@^4.1.0, object.assign@^4.1.4:
has-symbols "^1.0.3" has-symbols "^1.0.3"
object-keys "^1.1.1" object-keys "^1.1.1"
object.entries@^1.1.0: object.entries@^1.1.0, object.entries@^1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23" resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23"
integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w== integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==
@@ -13833,7 +14160,7 @@ object.entries@^1.1.0:
define-properties "^1.1.4" define-properties "^1.1.4"
es-abstract "^1.20.4" es-abstract "^1.20.4"
"object.fromentries@^2.0.0 || ^1.0.0": "object.fromentries@^2.0.0 || ^1.0.0", object.fromentries@^2.0.6:
version "2.0.6" version "2.0.6"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73"
integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==
@@ -13863,6 +14190,14 @@ object.getownpropertydescriptors@^2.1.0:
es-abstract "^1.21.2" es-abstract "^1.21.2"
safe-array-concat "^1.0.0" safe-array-concat "^1.0.0"
object.hasown@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92"
integrity sha512-B5UIT3J1W+WuWIU55h0mjlwaqxiE5vYENJXIXZ4VFe05pNYrkKuK0U/6aFcb0pKywYJh7IhfoqUfKVmrJJHZHw==
dependencies:
define-properties "^1.1.4"
es-abstract "^1.20.4"
object.pick@^1.3.0: object.pick@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
@@ -13870,7 +14205,7 @@ object.pick@^1.3.0:
dependencies: dependencies:
isobject "^3.0.1" isobject "^3.0.1"
object.values@^1.1.0: object.values@^1.1.0, object.values@^1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
@@ -14005,6 +14340,18 @@ optionator@^0.8.1:
type-check "~0.3.2" type-check "~0.3.2"
word-wrap "~1.2.3" word-wrap "~1.2.3"
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
dependencies:
deep-is "^0.1.3"
fast-levenshtein "^2.0.6"
levn "^0.4.1"
prelude-ls "^1.2.1"
type-check "^0.4.0"
word-wrap "^1.2.3"
ora@3.4.0: ora@3.4.0:
version "3.4.0" version "3.4.0"
resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318"
@@ -14890,16 +15237,33 @@ prebuild-install@^7.1.1:
tar-fs "^2.0.0" tar-fs "^2.0.0"
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prelude-ls@~1.1.2: prelude-ls@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
"prettier@>=2.2.1 <=2.3.0": "prettier@>=2.2.1 <=2.3.0":
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
prettier@^2.8.8:
version "2.8.8"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
pretty-bytes@5.6.0, pretty-bytes@^5.1.0: pretty-bytes@5.6.0, pretty-bytes@^5.1.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@@ -15050,7 +15414,7 @@ prompts@^2.0.1, prompts@^2.2.1, prompts@^2.3.2, prompts@^2.4.0:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.7.2: prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.7.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@@ -16034,6 +16398,15 @@ resolve@^1.10.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.18.1, resolve@^1.1
path-parse "^1.0.7" path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0" supports-preserve-symlinks-flag "^1.0.0"
resolve@^2.0.0-next.4:
version "2.0.0-next.4"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.4.tgz#3d37a113d6429f496ec4752d2a2e58efb1fd4660"
integrity sha512-iMDbmAWtfU+MHpxt/I5iWI7cY6YVEZUQ3MBgPQ++XD1PELuJHIl82xBmObyP2KyQmkNB2dsqF7seoQQiAn5yDQ==
dependencies:
is-core-module "^2.9.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
resolve@~1.7.1: resolve@~1.7.1:
version "1.7.1" version "1.7.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.7.1.tgz#aadd656374fd298aee895bc026b8297418677fd3"
@@ -16321,6 +16694,13 @@ semver@^7.0.0, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semve
dependencies: dependencies:
lru-cache "^6.0.0" lru-cache "^6.0.0"
semver@^7.3.7:
version "7.5.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec"
integrity sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==
dependencies:
lru-cache "^6.0.0"
send@0.18.0, send@^0.18.0: send@0.18.0, send@^0.18.0:
version "0.18.0" version "0.18.0"
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
@@ -16986,7 +17366,7 @@ string-width@^3.0.0, string-width@^3.1.0:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^5.1.0" strip-ansi "^5.1.0"
"string.prototype.matchall@^4.0.0 || ^3.0.1": "string.prototype.matchall@^4.0.0 || ^3.0.1", string.prototype.matchall@^4.0.8:
version "4.0.8" version "4.0.8"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3"
integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg== integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==
@@ -17128,6 +17508,11 @@ strip-indent@^3.0.0:
dependencies: dependencies:
min-indent "^1.0.0" min-indent "^1.0.0"
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
strip-json-comments@~2.0.1: strip-json-comments@~2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -17682,7 +18067,7 @@ ts-pnp@^1.1.6:
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
tslib@^1.13.0, tslib@^1.9.3: tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.3:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@@ -17697,6 +18082,13 @@ tslib@^2.5.3:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913"
integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w== integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
tty-browserify@0.0.0: tty-browserify@0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
@@ -17714,6 +18106,13 @@ tunnel@^0.0.6:
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
dependencies:
prelude-ls "^1.2.1"
type-check@~0.3.2: type-check@~0.3.2:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72"
@@ -18718,7 +19117,7 @@ wonka@^6.1.2:
resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.1.2.tgz#2c66fa5b26a12f002a03619b988258313d0b5352" resolved "https://registry.yarnpkg.com/wonka/-/wonka-6.1.2.tgz#2c66fa5b26a12f002a03619b988258313d0b5352"
integrity sha512-zNrXPMccg/7OEp9tSfFkMgTvhhowqasiSHdJ3eCZolXxVTV/aT6HUTofoZk9gwRbGoFey/Nss3JaZKUMKMbofg== integrity sha512-zNrXPMccg/7OEp9tSfFkMgTvhhowqasiSHdJ3eCZolXxVTV/aT6HUTofoZk9gwRbGoFey/Nss3JaZKUMKMbofg==
word-wrap@~1.2.3: word-wrap@^1.2.3, word-wrap@~1.2.3:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==