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

View File

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

View File

@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/ban-types */
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 { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { RootState, useSelector } from './state/Store';
@@ -23,8 +28,8 @@ import { Button, Center, VStack } from 'native-base';
import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton';
const protectedRoutes = () => ({
const protectedRoutes = () =>
({
Home: { component: HomeView, options: { title: translate('welcome'), headerLeft: null } },
Play: { component: PlayView, options: { title: translate('play') } },
Settings: { component: SetttingsNavigator, options: { title: 'Settings' } },
@@ -33,24 +38,29 @@ const protectedRoutes = () => ({
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;
} as const);
const publicRoutes = () => ({
Start: { component: StartPageView, options: { title: "Chromacase", headerShown: false } },
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;
} as const);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Route<Props = any> = {
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element),
options: any
}
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element);
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>> = {
[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 PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
@@ -58,34 +68,47 @@ type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
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']) =>
// eslint-disable-next-line react/display-name
(props: NativeStackScreenProps<T & ParamListBase>) =>
(
<>
{component({ ...props.route.params, route: props.route } as Parameters<Route<T>['component']>[0])}
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) => Object.entries(routes)
.map(([name, route], routeIndex) => (
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
Object.entries(routes).map(([name, route], routeIndex) => (
<Stack.Screen
key={'route-' + routeIndex}
name={name as keyof AppRouteParams}
options={route.options}
component={RouteToScreen(route.component)}
/>
))
));
const ProfileErrorView = (props: { onTryAgain: () => any }) => {
const ProfileErrorView = (props: { onTryAgain: () => void }) => {
const dispatch = useDispatch();
return <Center style={{ flexGrow: 1 }}>
return (
<Center style={{ flexGrow: 1 }}>
<VStack space={3}>
<Translate translationKey='userProfileFetchError'/>
<Button onPress={props.onTryAgain}><Translate translationKey='tryAgain'/></Button>
<TextButton onPress={() => dispatch(unsetAccessToken())}
colorScheme="error" variant='outline'
<Translate translationKey="userProfileFetchError" />
<Button onPress={props.onTryAgain}>
<Translate translationKey="tryAgain" />
</Button>
<TextButton
onPress={() => dispatch(unsetAccessToken())}
colorScheme="error"
variant="outline"
translate={{ translationKey: 'signOutBtn' }}
/>
</VStack>
</Center>
}
);
};
export const Router = () => {
const dispatch = useDispatch();
@@ -108,25 +131,27 @@ export const Router = () => {
}, [accessToken]);
return (
<NavigationContainer theme={colorScheme == 'light'
? DefaultTheme
: DarkTheme
}>
<NavigationContainer theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}>
<Stack.Navigator>
{ userProfile.isError && accessToken && !userProfile.isLoading
? <Stack.Screen name="Oops" component={RouteToScreen(() => <ProfileErrorView onTryAgain={() => userProfile.refetch()}/>)}/>
: userProfile.isLoading && !userProfile.data ?
{userProfile.isError && accessToken && !userProfile.isLoading ? (
<Stack.Screen
name="Oops"
component={RouteToScreen(() => (
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
) : userProfile.isLoading && !userProfile.data ? (
<Stack.Screen name="Loading" component={RouteToScreen(LoadingView)} />
: routesToScreens(userProfile.isSuccess && accessToken
? protectedRoutes()
: publicRoutes())
}
) : (
routesToScreens(
userProfile.isSuccess && accessToken ? protectedRoutes() : publicRoutes()
)
)}
</Stack.Navigator>
</NavigationContainer>
);
}
};
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();

View File

@@ -1,14 +1,16 @@
import { NativeBaseProvider, extendTheme, useColorMode, useTheme } from 'native-base';
import { NativeBaseProvider, extendTheme, useColorMode } from 'native-base';
import useColorScheme from './hooks/colorScheme';
import { useEffect } from 'react';
const ThemeProvider = ({ children }: { children: JSX.Element }) => {
const colorScheme = useColorScheme();
return <NativeBaseProvider theme={extendTheme({
return (
<NativeBaseProvider
theme={extendTheme({
config: {
useSystemColorMode: false,
initialColorMode: colorScheme
initialColorMode: colorScheme,
},
colors: {
primary: {
@@ -58,24 +60,25 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
700: '#810000',
800: '#500000',
900: '#210000',
}
},
},
components: {
Button: {
variants: {
solid: () => ({
rounded: 'full',
})
}
}
}
})}>
}),
},
},
},
})}
>
{children}
</NativeBaseProvider>;
}
const ColorSchemeProvider = (props: { children: any }) => {
</NativeBaseProvider>
);
};
const ColorSchemeProvider = (props: { children: JSX.Element }) => {
const colorScheme = useColorScheme();
const colorMode = useColorMode();
@@ -83,7 +86,7 @@ const ColorSchemeProvider = (props: { children: any }) => {
colorMode.setColorMode(colorScheme);
}, [colorScheme]);
return props.children;
}
};
export default ThemeProvider;
export { ColorSchemeProvider };

View File

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

View File

@@ -14,9 +14,7 @@
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": [
"**/*"
],
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ import React from 'react';
import { FlatGrid } from 'react-native-super-grid';
import { Heading, VStack } from 'native-base';
type CardGridCustomProps<T> = {
content: T[];
heading?: JSX.Element;
@@ -11,7 +10,7 @@ type CardGridCustomProps<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;
return (

View File

@@ -1,6 +1,6 @@
import { useNavigation } from "../Navigation";
import { HStack, VStack, Text, Progress } from "native-base";
import { translate } from "../i18n/i18n";
import { useNavigation } from '../Navigation';
import { HStack, VStack, Text, Progress } from 'native-base';
import { translate } from '../i18n/i18n';
import Card from './Card';
type CompetenciesTableProps = {
@@ -10,7 +10,7 @@ type CompetenciesTableProps = {
accuracyCompetency: number;
arpegeCompetency: number;
chordsCompetency: number;
}
};
const CompetenciesTable = (props: CompetenciesTableProps) => {
const navigation = useNavigation();
@@ -19,17 +19,23 @@ const CompetenciesTable = (props: CompetenciesTableProps) => {
<HStack space={5} flex={1}>
<VStack space={5}>
{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 space={5} flex={1}>
{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>
</HStack>
</Card>
)
}
);
};
export default CompetenciesTable
export default CompetenciesTable;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,9 @@ type SearchHistoryCardProps = {
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 handlePress = () => {
@@ -21,12 +23,12 @@ const SearchHistoryCard = (props: SearchHistoryCardProps & { onPress: (query: st
<Card shadow={2} onPress={handlePress}>
<VStack m={1.5} space={3}>
<Text fontSize="lg" fontWeight="bold">
{query ?? "query"}
{query ?? 'query'}
</Text>
<Text fontSize="lg" fontWeight="semibold">
{type ?? "type"}
{type ?? 'type'}
</Text>
<Text color="gray.500">{timestamp ?? "timestamp"}</Text>
<Text color="gray.500">{timestamp ?? 'timestamp'}</Text>
</VStack>
</Card>
);

View File

@@ -1,12 +1,16 @@
import { Box, Button } from "native-base";
import { Box, Button } from 'native-base';
type IconButtonProps = {
icon: Parameters<typeof Button>[0]['leftIcon']
icon: Parameters<typeof Button>[0]['leftIcon'];
} & Omit<Parameters<typeof Button>[0], 'leftIcon' | 'rightIcon'>;
// Wrapper around Button for IconButton as Native's one sucks <3
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;

View File

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

View File

@@ -1,15 +1,17 @@
import { useTheme } from "native-base";
import { Center, Spinner } from "native-base";
import { useTheme } from 'native-base';
import { Center, Spinner } from 'native-base';
const LoadingComponent = () => {
const theme = useTheme();
return <Spinner color={theme.colors.primary[500]}/>
}
return <Spinner color={theme.colors.primary[500]} />;
};
const LoadingView = () => {
return <Center style={{ flexGrow: 1 }}>
return (
<Center style={{ flexGrow: 1 }}>
<LoadingComponent />
</Center>
}
);
};
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
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
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 { useWindowDimensions } from 'react-native';
import SoundFont from 'soundfont-player';
@@ -14,7 +21,7 @@ type PartitionViewProps = {
onEndReached: () => void;
// Timestamp of the play session, in milisecond
timestamp: number;
}
};
const PartitionView = (props: PartitionViewProps) => {
const [osmd, setOsmd] = useState<OSMD>();
@@ -34,15 +41,15 @@ const PartitionView = (props: PartitionViewProps) => {
renderSingleHorizontalStaffline: true,
cursorsOptions: [{ type: CursorType.Standard, color: 'green', alpha: 0.5, follow: false }],
autoResize: false,
}
};
// Turns note.Length or timestamp in ms
const timestampToMs = (timestamp: Fraction) => {
return timestamp.RealValue * wholeNoteLength;
}
};
const getActualNoteLength = (note: Note) => {
let duration = timestampToMs(note.Length)
let duration = timestampToMs(note.Length);
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) {
duration += timestampToMs(firstNote.Length);
} else {
@@ -50,38 +57,46 @@ const PartitionView = (props: PartitionViewProps) => {
}
}
return duration;
}
};
const playNotesUnderCursor = () => {
osmd!.cursor.NotesUnderCursor()
osmd!.cursor
.NotesUnderCursor()
.filter((note) => note.isRest() == false)
.filter((note) => note.Pitch) // Pitch Can be null, avoiding them
.forEach((note) => {
// 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;
// console.log('Expecting midi ' + midiNumber);
let duration = getActualNoteLength(note);
const duration = getActualNoteLength(note);
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
soundPlayer!.play(midiNumber.toString(), audioContext.currentTime, { duration, gain })
soundPlayer!.play(midiNumber.toString(), audioContext.currentTime, {
duration,
gain,
});
}
});
};
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(() => {
const _osmd = new OSMD(OSMD_DIV_ID, options);
Promise.all([
SoundFont.instrument(audioContext as unknown as AudioContext, 'electric_piano_1'),
_osmd.load(props.file)
]).then(([player, __]) => {
_osmd.load(props.file),
]).then(([player]) => {
setSoundPlayer(player);
_osmd.render();
_osmd.cursor.hide();
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
setWholeNoteLength(Math.round((60 / bpm) * 4000))
setWholeNoteLength(Math.round((60 / bpm) * 4000));
props.onPartitionReady();
// Do not show cursor before actuall start
});
@@ -96,7 +111,7 @@ const PartitionView = (props: PartitionViewProps) => {
osmd.cursor.show();
}
}
}, [dimensions])
}, [dimensions]);
useEffect(() => {
if (!osmd || !soundPlayer) {
@@ -110,10 +125,14 @@ const PartitionView = (props: PartitionViewProps) => {
let previousCursorPosition = -1;
let currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
let shortestNote = getShortedNoteUnderCursor();
while(!osmd.cursor.iterator.EndReached && (shortestNote?.isRest
while (
!osmd.cursor.iterator.EndReached &&
(shortestNote?.isRest
? timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) +
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) < props.timestamp
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) < props.timestamp)
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) <
props.timestamp
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) <
props.timestamp)
) {
previousCursorPosition = currentCursorPosition;
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
playNotesUnderCursor();
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();
}
}
}, [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;

View File

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

View File

@@ -1,18 +1,9 @@
import {
useTheme,
Box,
Image,
Row,
Column,
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";
import { Box, Image, Row, Column, Button, Icon } from 'native-base';
import IconButton from '../IconButton';
import { MotiView, useDynamicAnimation } from 'moti';
import { Easing } from 'react-native-reanimated';
import React from 'react';
import { FontAwesome5, MaterialCommunityIcons } from '@expo/vector-icons';
type ImgSlideViewProps = {
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) => {
// 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 stepSize = speed / 2;
const stepDuration = 1000 / 2;
@@ -45,7 +38,7 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
animation.animateTo({
translateX: -nbPixelsToSkip,
transition: {
type: "timing",
type: 'timing',
delay: 0,
easing: Easing.linear,
},
@@ -69,16 +62,11 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
return (
<Column>
<Box overflow={"hidden"}>
<Box overflow={'hidden'}>
<MotiView
state={animation}
onDidAnimate={(
styleProp,
didAnimationFinish,
_maybeValue,
{ attemptedValue }
) => {
if (styleProp === "translateX" && didAnimationFinish) {
onDidAnimate={(styleProp, didAnimationFinish) => {
if (styleProp === 'translateX' && didAnimationFinish) {
stepCount++;
}
}}
@@ -102,11 +90,9 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
icon={<Icon as={FontAwesome5} name="play" size="sm" />}
onPress={() => {
animation.animateTo({
translateX: range(-totalWidth, 0, stepSize)
.reverse()
.slice(stepCount),
translateX: range(-totalWidth, 0, stepSize).reverse().slice(stepCount),
transition: {
type: "timing",
type: 'timing',
easing: Easing.linear,
duration: stepDuration,
},
@@ -120,19 +106,11 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
}}
/>
<IconButton
icon={
<Icon as={MaterialCommunityIcons} name="rewind-10" size="sm" />
}
icon={<Icon as={MaterialCommunityIcons} name="rewind-10" size="sm" />}
onPress={() => jumpAt(-200, false)}
/>
<IconButton
icon={
<Icon
as={MaterialCommunityIcons}
name="fast-forward-10"
size="sm"
/>
}
icon={<Icon as={MaterialCommunityIcons} name="fast-forward-10" size="sm" />}
onPress={() => jumpAt(200, false)}
/>
<IconButton

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { Text } from "native-base";
import { translate } from "../i18n/i18n";
import { en } from "../i18n/Translations";
import { RootState, useSelector } from "../state/Store";
import { Text } from 'native-base';
import { translate } from '../i18n/i18n';
import { en } from '../i18n/Translations';
import { RootState, useSelector } from '../state/Store';
type TranslateProps = {
translationKey: keyof typeof en;
@@ -17,6 +17,6 @@ const Translate = ({ translationKey, format, ...props }: TranslateProps) => {
const translated = translate(translationKey, selectedLanguage);
return <Text {...props}>{format ? format(translated) : translated}</Text>;
}
};
export default Translate;

View File

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

View File

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

View File

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

View File

@@ -1,37 +1,26 @@
import React from "react";
import { translate } from "../../i18n/i18n";
import { string } from "yup";
import {
FormControl,
Input,
Stack,
WarningOutlineIcon,
Box,
Button,
useToast,
} from "native-base";
import React from 'react';
import { translate } from '../../i18n/i18n';
import { string } from 'yup';
import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base';
interface ChangeEmailFormProps {
onSubmit: (
oldEmail: string,
newEmail: string
) => Promise<string>;
onSubmit: (oldEmail: string, newEmail: string) => Promise<string>;
}
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 [formData, setFormData] = React.useState({
oldEmail: {
value: "",
value: '',
error: null as string | null,
},
newEmail: {
value: "",
value: '',
error: null as string | null,
}
},
});
const [submittingForm, setSubmittingForm] = React.useState(false);
@@ -42,16 +31,13 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl
isRequired
isInvalid={
formData.oldEmail.error !== null ||
formData.newEmail.error !== null
}
isInvalid={formData.oldEmail.error !== null || formData.newEmail.error !== null}
>
<FormControl.Label>{translate("oldEmail")}</FormControl.Label>
<FormControl.Label>{translate('oldEmail')}</FormControl.Label>
<Input
isRequired
type="text"
placeholder={translate("oldEmail")}
placeholder={translate('oldEmail')}
value={formData.oldEmail.value}
onChangeText={(t) => {
let error: null | string = null;
@@ -67,11 +53,11 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
{formData.oldEmail.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("newEmail")}</FormControl.Label>
<FormControl.Label>{translate('newEmail')}</FormControl.Label>
<Input
isRequired
type="text"
placeholder={translate("newEmail")}
placeholder={translate('newEmail')}
value={formData.newEmail.value}
onChangeText={(t) => {
let error: null | string = null;
@@ -90,13 +76,12 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
<Button
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={
formData.newEmail.error !== null
}
isDisabled={formData.newEmail.error !== null}
onPress={async () => {
setSubmittingForm(true);
try {
const resp = await onSubmit(formData.oldEmail.value,
const resp = await onSubmit(
formData.oldEmail.value,
formData.newEmail.value
);
toast.show({ description: resp });
@@ -107,12 +92,12 @@ const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
}
}}
>
{translate("submitBtn")}
{translate('submitBtn')}
</Button>
</FormControl>
</Stack>
</Box>
);
}
};
export default ChangeEmailForm;

View File

@@ -1,36 +1,24 @@
import React from "react";
import { translate } from "../../i18n/i18n";
import { string } from "yup";
import {
FormControl,
Input,
Stack,
WarningOutlineIcon,
Box,
Button,
useToast,
} from "native-base";
import React from 'react';
import { translate } from '../../i18n/i18n';
import { string } from 'yup';
import { FormControl, Input, Stack, WarningOutlineIcon, Box, Button, useToast } from 'native-base';
interface ChangePasswordFormProps {
onSubmit: (
oldPassword: string,
newPassword: string
) => Promise<string>;
onSubmit: (oldPassword: string, newPassword: string) => Promise<string>;
}
const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
const [formData, setFormData] = React.useState({
oldPassword: {
value: "",
value: '',
error: null as string | null,
},
newPassword: {
value: "",
value: '',
error: null as string | null,
},
confirmNewPassword: {
value: "",
value: '',
error: null as string | null,
},
});
@@ -38,9 +26,9 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
const validationSchemas = {
password: string()
.min(4, translate("passwordTooShort"))
.max(100, translate("passwordTooLong"))
.required("Password is required"),
.min(4, translate('passwordTooShort'))
.max(100, translate('passwordTooLong'))
.required('Password is required'),
};
const toast = useToast();
@@ -52,14 +40,14 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
isInvalid={
formData.oldPassword.error !== null ||
formData.newPassword.error !== null ||
formData.confirmNewPassword.error !== null}
formData.confirmNewPassword.error !== null
}
>
<FormControl.Label>{translate("oldPassword")}</FormControl.Label>
<FormControl.Label>{translate('oldPassword')}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("oldPassword")}
placeholder={translate('oldPassword')}
value={formData.oldPassword.value}
onChangeText={(t) => {
let error: null | string = null;
@@ -75,11 +63,11 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
{formData.oldPassword.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("newPassword")}</FormControl.Label>
<FormControl.Label>{translate('newPassword')}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("newPassword")}
placeholder={translate('newPassword')}
value={formData.newPassword.value}
onChangeText={(t) => {
let error: null | string = null;
@@ -95,19 +83,19 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
{formData.newPassword.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("confirmNewPassword")}</FormControl.Label>
<FormControl.Label>{translate('confirmNewPassword')}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("confirmNewPassword")}
placeholder={translate('confirmNewPassword')}
value={formData.confirmNewPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.catch((e) => (error = e.message));
if (!error && t !== formData.newPassword.value) {
error = translate("passwordsDontMatch");
error = translate('passwordsDontMatch');
}
setFormData({
...formData,
@@ -126,9 +114,9 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
formData.oldPassword.error !== null ||
formData.newPassword.error !== null ||
formData.confirmNewPassword.error !== null ||
formData.oldPassword.value === "" ||
formData.newPassword.value === "" ||
formData.confirmNewPassword.value === ""
formData.oldPassword.value === '' ||
formData.newPassword.value === '' ||
formData.confirmNewPassword.value === ''
}
onPress={async () => {
setSubmittingForm(true);
@@ -145,13 +133,12 @@ const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
}
}}
>
{translate("submitBtn")}
{translate('submitBtn')}
</Button>
</FormControl>
</Stack>
</Box>
);
}
};
export default ChangePasswordForm;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { RootState, useSelector } from "../state/Store";
import i18n from "./i18n";
import { RootState, useSelector } from '../state/Store';
import i18n from './i18n';
type LanguageGateProps = {
children: any;
}
children: JSX.Element;
};
/**
* 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);
i18n.changeLanguage(language);
return props.children;
}
};
export default LanguageGate;

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import Model from "./Model";
import Model from './Model';
interface Artist extends Model {
name: string;

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import Skill from "./Skill";
import Model from "./Model";
import Skill from './Skill';
import Model from './Model';
/**
* 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
*/
title: string,
title: string;
/**
* Short description of the lesson
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
import Model from "./Model";
import SongDetails from "./SongDetails";
import Artist from "./Artist";
import Model from './Model';
import SongDetails from './SongDetails';
import Artist from './Artist';
interface Song extends Model {
id: number;

View File

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

View File

@@ -1,6 +1,6 @@
import UserData from "./UserData";
import Model from "./Model";
import UserSettings from "./UserSettings";
import UserData from './UserData';
import Model from './Model';
import UserSettings from './UserSettings';
interface User extends Model {
name: string;

View File

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

View File

@@ -8,6 +8,9 @@
"ios": "expo start --ios",
"web": "expo start --web",
"eject": "expo eject",
"pretty:check": "prettier --check",
"pretty:write": "prettier --write",
"lint": "eslint .",
"test": "jest -i",
"test:cov": "jest -i --coverage",
"test:watch": "jest -i --watch",
@@ -87,6 +90,13 @@
"babel-loader": "^8.3.0",
"babel-plugin-transform-inline-environment-variables": "^0.4.4",
"chromatic": "^6.14.0",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.31.11",
"prettier": "^2.8.8",
"react-test-renderer": "17.0.2",
"typescript": "^4.6.3"
},

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ import { AccessToken } from '../API';
export const userSlice = createSlice({
name: 'user',
initialState: {
accessToken: undefined as AccessToken | undefined
accessToken: undefined as AccessToken | undefined,
},
reducers: {
setAccessToken: (state, action: PayloadAction<AccessToken>) => {

View File

@@ -12,9 +12,12 @@
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"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. */
"jsx": "react-native", /* Specify what JSX code is generated. */
"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. */,
"jsx": "react-native" /* Specify what JSX code is generated. */,
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
@@ -25,20 +28,24 @@
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* 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. */
"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. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
"types": ["react-native", "jest", "node"], /* Specify type package names to be included without being referenced in a source file. */
"types": [
"react-native",
"jest",
"node"
] /* Specify type package names to be included without being referenced in a source file. */,
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
"resolveJsonModule": true, /* Enable importing .json files */
"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 */
"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. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
@@ -50,7 +57,7 @@
// "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. */
// "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. */
// "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. */
@@ -68,29 +75,29 @@
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
"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. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
"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. */,
"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. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
"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`. */
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
"strict": true /* Enable all strict type-checking options. */,
"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`. */,
"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. */
// "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`. */
"useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
"alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
"noUnusedLocals": false, /* Enable error reporting when a local variables aren't read. */
"noUnusedParameters": false, /* Raise an error when a function parameter isn't read */
"noImplicitThis": true /* Enable error reporting when `this` is given the type `any`. */,
"useUnknownInCatchVariables": true /* Type catch clause variables as 'unknown' instead of 'any'. */,
"alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
"noUnusedLocals": false /* Enable error reporting when a local variables aren'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'. */
"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. */
"noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
"noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
"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. */,
"noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */,
"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 */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
@@ -100,8 +107,11 @@
"skipLibCheck": true /* Skip type checking all .d.ts files. */
},
"exclude": [
"node_modules", "babel.config.js", "metro.config.js",
"jest.config.js", "app.config.ts",
"node_modules",
"babel.config.js",
"metro.config.js",
"jest.config.js",
"app.config.ts",
"*/*.test.tsx"
]
}

View File

@@ -1,16 +1,20 @@
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 { SafeAreaView } from 'react-native';
import { useQuery } from 'react-query';
import LoadingComponent from '../components/Loading';
import API from '../API';
const handleFavorite = () => {
const handleFavorite = () => {};
type ArtistDetailsViewProps = {
artistId: number;
};
const ArtistDetailsView = ({ artistId }: any) => {
const { isLoading, data: artistData, error } = useQuery(['artist', artistId], () => API.getArtist(artistId));
const ArtistDetailsView = ({ artistId }: ArtistDetailsViewProps) => {
const { isLoading, data: artistData } = useQuery(['artist', artistId], () =>
API.getArtist(artistId)
);
if (isLoading) {
return <LoadingComponent />;

View File

@@ -7,7 +7,11 @@ import AuthenticationView from '../views/AuthenticationView';
describe('<AuthenticationView />', () => {
it('has 3 children', () => {
const tree = TestRenderer.create(<Provider store={store}><AuthenticationView /></Provider>).toJSON();
const tree = TestRenderer.create(
<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 { Translate, translate } from "../i18n/i18n";
import API, { APIError } from "../API";
import { setAccessToken } from "../state/UserSlice";
import { Translate, translate } from '../i18n/i18n';
import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { Center, Button, Text } from 'native-base';
import SigninForm from "../components/forms/signinform";
import SignupForm from "../components/forms/signupform";
import TextButton from "../components/TextButton";
import { RouteProps } from "../Navigation";
import SigninForm from '../components/forms/signinform';
import SignupForm from '../components/forms/signupform';
import TextButton from '../components/TextButton';
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 {
const apiAccess = await API.authenticate({ username, password });
apiSetter(apiAccess);
return translate("loggedIn");
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
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 {
const apiAccess = await API.createAccount({ username, password, email });
apiSetter(apiAccess);
return translate("loggedIn");
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate("unknownError");
return translate('unknownError');
}
};
type AuthenticationViewProps = {
isSignup: boolean;
}
};
const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) => {
const dispatch = useDispatch();
const [mode, setMode] = React.useState<"signin" | "signup">(isSignup ? "signup" : "signin");
const [mode, setMode] = React.useState<'signin' | 'signup'>(isSignup ? 'signup' : 'signin');
return (
<Center style={{ flex: 1 }}>
<Text><Translate translationKey='welcome'/></Text>
{mode === "signin"
? <SigninForm onSubmit={(username, password) => hanldeSignin(username, password, (accessToken) => dispatch(setAccessToken(accessToken)))} />
: <SignupForm onSubmit={(username, password, email) => handleSignup(username, password, email, (accessToken) => dispatch(setAccessToken(accessToken)))} />
<Text>
<Translate translationKey="welcome" />
</Text>
{mode === 'signin' ? (
<SigninForm
onSubmit={(username, password) =>
hanldeSignin(username, password, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
{ mode ==="signin" && <Button variant="outline" marginTop={5} colorScheme="error" >{translate("forgottenPassword")}</Button> }
/>
) : (
<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
translate={{ translationKey: mode === "signin" ? "signUpBtn" : "signInBtn" }}
variant='outline' marginTop={5} colorScheme='primary'
onPress={() => setMode(mode === "signin" ? "signup" : "signin")}
translate={{ translationKey: mode === 'signin' ? 'signUpBtn' : 'signInBtn' }}
variant="outline"
marginTop={5}
colorScheme="primary"
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
/>
</Center>
);

View File

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

View File

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

View File

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

View File

@@ -1,57 +1,83 @@
import React from 'react';
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 { useNavigation } from "../Navigation";
import { useNavigation } from '../Navigation';
import TextButton from '../components/TextButton';
const UserMedals = () => {
return (
<Card marginX={20} marginY={10}>
<Heading>
<Translate translationKey='medals'/>
<Translate translationKey="medals" />
</Heading>
<HStack alignItems={'row'} space='10'>
<Image source={{
uri: "https://wallpaperaccess.com/full/317501.jpg"
}} alt="Profile picture" size="lg"
<HStack alignItems={'row'} space="10">
<Image
source={{
uri: 'https://wallpaperaccess.com/full/317501.jpg',
}}
alt="Profile picture"
size="lg"
/>
<Image source={{
uri: "https://wallpaperaccess.com/full/317501.jpg"
}} alt="Profile picture" size="lg"
<Image
source={{
uri: 'https://wallpaperaccess.com/full/317501.jpg',
}}
alt="Profile picture"
size="lg"
/>
<Image source={{
uri: "https://wallpaperaccess.com/full/317501.jpg"
}} alt="Profile picture" size="lg"
<Image
source={{
uri: 'https://wallpaperaccess.com/full/317501.jpg',
}}
alt="Profile picture"
size="lg"
/>
<Image source={{
uri: "https://wallpaperaccess.com/full/317501.jpg"
}} alt="Profile picture" size="lg"
<Image
source={{
uri: 'https://wallpaperaccess.com/full/317501.jpg',
}}
alt="Profile picture"
size="lg"
/>
</HStack>
</Card>
);
}
};
const PlayerStats = () => {
const answer = "Answer from back";
const answer = 'Answer from back';
return (
<Card marginX={20} marginY={10}>
<Heading> <Translate translationKey='playerStats'/> </Heading>
<Text> <Translate translationKey='mostPlayedSong'/> {answer} </Text>
<Text> <Translate translationKey='goodNotesPlayed'/> {answer} </Text>
<Text> <Translate translationKey='longestCombo'/> {answer} </Text>
<Text> <Translate translationKey='favoriteGenre'/> {answer} </Text>
<Heading>
{' '}
<Translate translationKey="playerStats" />{' '}
</Heading>
<Text>
{' '}
<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 profilePic = "https://wallpaperaccess.com/full/317501.jpg"
const username = "Username"
const level = "1"
const profilePic = 'https://wallpaperaccess.com/full/317501.jpg';
const username = 'Username';
const level = '1';
// banner size
const dimensions = Dimensions.get('window');
@@ -62,12 +88,17 @@ const ProfilePictureBannerAndLevel = () => {
return (
<View style={{ flexDirection: 'row' }}>
<Image source={{ uri : "https://wallpaperaccess.com/full/317501.jpg" }} size="lg"
<Image
source={{ uri: 'https://wallpaperaccess.com/full/317501.jpg' }}
size="lg"
style={{ height: imageHeight, width: imageWidth, zIndex: 0, opacity: 0.5 }}
/>
<Box zIndex={1} position={"absolute"} marginY={10} marginX={10}>
<Image borderRadius={100} source={{ uri: profilePic }}
alt="Profile picture" size="lg"
<Box zIndex={1} position={'absolute'} marginY={10} marginX={10}>
<Image
borderRadius={100}
source={{ uri: profilePic }}
alt="Profile picture"
size="lg"
style={{ position: 'absolute' }}
/>
<Box w="100%" paddingY={3} paddingLeft={100}>
@@ -77,7 +108,7 @@ const ProfilePictureBannerAndLevel = () => {
</Box>
</View>
);
}
};
const ProfileView = () => {
const navigation = useNavigation();
@@ -95,6 +126,6 @@ const ProfileView = () => {
</Box>
</View>
);
}
};
export default ProfileView;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,48 +1,43 @@
import API from "../../API";
import { useDispatch } from "react-redux";
import { unsetAccessToken } from "../../state/UserSlice";
import React from "react";
import {
Column,
Text,
Button,
Box,
Flex,
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";
import API from '../../API';
import { useDispatch } from 'react-redux';
import { unsetAccessToken } from '../../state/UserSlice';
import React from 'react';
import { Column, Text, Button, Box, Flex, 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) => {
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 userQuery = useQuery(['user'], () => API.getUserInfo());
const dispatch = useDispatch();
if (!userQuery.data || userQuery.isLoading) {
return <LoadingView/>
return <LoadingView />;
}
const user = userQuery.data;
return (
<Flex
style={{
flex: 1,
alignItems: "center",
alignItems: 'center',
paddingTop: 40,
}}
>
<Column
style={{
width: "100%",
alignItems: "center",
width: '100%',
alignItems: 'center',
}}
>
<Center>
@@ -53,17 +48,17 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<ElementList
style={{
marginTop: 20,
width: "90%",
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: "text",
title: translate("email"),
type: 'text',
title: translate('email'),
data: {
text: user.email || translate("NoAssociatedEmail"),
text: user.email || translate('NoAssociatedEmail'),
onPress: () => {
navigation.navigate("ChangeEmail");
navigation.navigate('ChangeEmail');
},
},
},
@@ -73,54 +68,54 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<ElementList
style={{
marginTop: 20,
width: "90%",
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: "text",
title: translate("username"),
type: 'text',
title: translate('username'),
data: {
text: user.name,
},
},
{
type: "text",
title: "ID",
helperText: "This is your unique ID, be proud of it!",
type: 'text',
title: 'ID',
helperText: 'This is your unique ID, be proud of it!',
data: {
text: user.id.toString(),
},
},
{
type: "text",
title: translate("nbGamesPlayed"),
type: 'text',
title: translate('nbGamesPlayed'),
data: {
text: user.data.gamesPlayed.toString(),
},
},
{
type: "text",
title: "XP",
description: translate("XPDescription"),
type: 'text',
title: 'XP',
description: translate('XPDescription'),
data: {
text: user.data.xp.toString(),
},
},
{
type: "text",
title: translate("userCreatedAt"),
type: 'text',
title: translate('userCreatedAt'),
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: {
text: user.data.createdAt.toLocaleDateString(),
},
},
{
type: "text",
title: translate("premiumAccount"),
type: 'text',
title: translate('premiumAccount'),
data: {
text: translate(user.premium ? "yes" : "no"),
text: translate(user.premium ? 'yes' : 'no'),
},
},
]}
@@ -131,17 +126,17 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<ElementList
style={{
marginTop: 10,
width: "90%",
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: "toggle",
title: "Piano Magique",
type: 'toggle',
title: 'Piano Magique',
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:
"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,
data: {
value: false,
@@ -149,20 +144,20 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
},
},
{
type: "dropdown",
title: "Thème de piano",
type: 'dropdown',
title: 'Thème de piano',
disabled: true,
data: {
value: "default",
value: 'default',
onSelect: () => {},
options: [
{
label: "Default",
value: "default",
label: 'Default',
value: 'default',
},
{
label: "Catpuccino",
value: "catpuccino",
label: 'Catpuccino',
value: 'catpuccino',
},
],
},
@@ -176,41 +171,39 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
<TextButton
onPress={() => dispatch(unsetAccessToken())}
translate={{
translationKey: "signOutBtn",
translationKey: 'signOutBtn',
}}
/>
)}
{user.isGuest && (
<Popover
trigger={(triggerProps) => (
<Button {...triggerProps}>{translate("signOutBtn")}</Button>
<Button {...triggerProps}>{translate('signOutBtn')}</Button>
)}
>
<Popover.Content>
<Popover.Arrow />
<Popover.Body>
<Heading size="md" mb={2}>
{translate("Attention")}
{translate('Attention')}
</Heading>
<Text>
{translate(
"YouAreCurrentlyConnectedWithAGuestAccountWarning"
)}
{translate('YouAreCurrentlyConnectedWithAGuestAccountWarning')}
</Text>
<Button.Group variant="ghost" space={2}>
<Button
onPress={() => dispatch(unsetAccessToken())}
colorScheme="red"
>
{translate("signOutBtn")}
{translate('signOutBtn')}
</Button>
<Button
onPress={() => {
navigation.navigate("GuestToUser");
navigation.navigate('GuestToUser');
}}
colorScheme="green"
>
{translate("signUpBtn")}
{translate('signUpBtn')}
</Button>
</Button.Group>
</Popover.Body>

View File

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

View File

@@ -1,4 +1,4 @@
const createExpoWebpackConfigAsync = require('@expo/webpack-config')
const createExpoWebpackConfigAsync = require('@expo/webpack-config');
module.exports = async function (env, argv) {
const config = await createExpoWebpackConfigAsync(
@@ -7,11 +7,9 @@ module.exports = async function (env, argv) {
babel: { dangerouslyAddModulePathsToTranspile: ['moti'] },
},
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"
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":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@expo/bunyan/-/bunyan-4.0.0.tgz#be0c1de943c7987a9fbd309ea0b1acd605890c7b"
@@ -1899,6 +1931,25 @@
dependencies:
"@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":
version "3.0.2"
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"
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"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
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"
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":
version "3.1.4"
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"
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@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@@ -4927,6 +4988,90 @@
dependencies:
"@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":
version "2.3.6"
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"
integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==
acorn-jsx@^5.3.1:
acorn-jsx@^5.3.1, acorn-jsx@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
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"
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"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
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"
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"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
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"
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"
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
@@ -5744,7 +5889,7 @@ array.prototype.flat@^1.2.1:
es-abstract "^1.20.4"
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"
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
@@ -5776,6 +5921,17 @@ array.prototype.reduce@^1.0.5:
es-array-method-boxes-properly "^1.0.0"
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:
version "2.0.1"
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
@@ -7435,7 +7591,7 @@ cross-fetch@^3.1.5:
dependencies:
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"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
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"
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"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
@@ -8065,6 +8221,13 @@ dns-txt@^2.0.2:
dependencies:
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:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@@ -8572,7 +8735,40 @@ escodegen@^2.0.0:
optionalDependencies:
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"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
@@ -8588,11 +8784,85 @@ eslint-scope@^4.0.3:
esrecurse "^4.1.0"
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:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
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:
version "4.3.0"
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"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.2.0:
estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
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"
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:
version "2.2.7"
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"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-levenshtein@~2.0.6:
fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
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"
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:
version "6.2.0"
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:
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:
version "3.4.0"
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"
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:
version "1.0.3"
resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf"
@@ -9839,7 +10135,7 @@ globby@11.0.1:
merge2 "^1.3.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"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b"
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"
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:
version "2.12.6"
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"
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"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@@ -10973,7 +11274,7 @@ is-glob@^3.0.0, is-glob@^3.1.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
@@ -11060,7 +11361,7 @@ is-path-inside@^2.1.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283"
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"
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:
version "3.3.3"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81"
@@ -12087,6 +12393,14 @@ jsonfile@^6.0.1:
optionalDependencies:
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:
version "3.10.1"
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"
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:
version "0.3.0"
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"
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"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -13455,6 +13777,11 @@ native-base@^3.4.17:
tinycolor2 "^1.4.2"
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:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -13814,7 +14141,7 @@ object-visit@^1.0.0:
dependencies:
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"
resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f"
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"
object-keys "^1.1.1"
object.entries@^1.1.0:
object.entries@^1.1.0, object.entries@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.6.tgz#9737d0e5b8291edd340a3e3264bb8a3b00d5fa23"
integrity sha512-leTPzo4Zvg3pmbQ3rDK69Rl8GQvIqMWubrkxONG9/ojtFE2rD9fjMKfSI5BxW3osRH1m6VdzmqK8oAY9aT4x5w==
@@ -13833,7 +14160,7 @@ object.entries@^1.1.0:
define-properties "^1.1.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"
resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73"
integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg==
@@ -13863,6 +14190,14 @@ object.getownpropertydescriptors@^2.1.0:
es-abstract "^1.21.2"
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:
version "1.3.0"
resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747"
@@ -13870,7 +14205,7 @@ object.pick@^1.3.0:
dependencies:
isobject "^3.0.1"
object.values@^1.1.0:
object.values@^1.1.0, object.values@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==
@@ -14005,6 +14340,18 @@ optionator@^0.8.1:
type-check "~0.3.2"
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:
version "3.4.0"
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"
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:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
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":
version "2.3.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
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:
version "5.6.0"
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"
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"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
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"
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:
version "1.7.1"
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:
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:
version "0.18.0"
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"
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"
resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.8.tgz#3bf85722021816dcd1bf38bb714915887ca79fd3"
integrity sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==
@@ -17128,6 +17508,11 @@ strip-indent@^3.0.0:
dependencies:
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:
version "2.0.1"
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"
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"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
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"
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:
version "0.0.0"
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"
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:
version "0.3.2"
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"
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"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==