Restore guest mode

This commit is contained in:
mathysPaul
2023-10-08 19:47:03 +02:00
parent 1228eb603e
commit f610de3045
28 changed files with 855 additions and 1062 deletions

View File

@@ -11,7 +11,6 @@ import { RootState, useSelector } from './state/Store';
import { useDispatch } from 'react-redux';
import { Translate, translate } from './i18n/i18n';
import SongLobbyView from './views/SongLobbyView';
import StartPageView from './views/StartPageView';
import HomeView from './views/HomeView';
import SearchView from './views/SearchView';
import SetttingsNavigator from './views/settings/SettingsView';
@@ -34,7 +33,6 @@ import SigninView from './views/SigninView';
import SignupView from './views/SignupView';
import PasswordResetView from './views/PasswordResetView';
import ForgotPasswordView from './views/ForgotPasswordView';
import ScaffoldCC from './components/UI/ScaffoldCC';
import DiscoveryView from './views/V2/DiscoveryView';
// Util function to hide route props in URL
@@ -101,11 +99,11 @@ const protectedRoutes = () =>
const publicRoutes = () =>
({
Start: {
component: StartPageView,
options: { title: 'Chromacase', headerShown: false },
link: '/',
},
// Start: {
// component: StartPageView,
// options: { title: 'Chromacase', headerShown: false },
// link: '/',
// },
Login: {
component: SigninView,
options: { title: translate('signInBtn'), headerShown: false },
@@ -165,13 +163,9 @@ const RouteToScreen =
(props: NativeStackScreenProps<T & ParamListBase>) =>
(
<>
<ScaffoldCC routeName={props.route.name}>
<>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
</ScaffoldCC>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);

View File

@@ -4,6 +4,8 @@ import InteractiveBase from './InteractiveBase';
import { Text, useTheme } from 'native-base';
import { Icon } from 'iconsax-react-native';
export type ButtonType = 'filled' | 'outlined' | 'menu';
interface ButtonProps {
title?: string;
style?: StyleProp<ViewStyle>;
@@ -12,7 +14,7 @@ interface ButtonProps {
icon?: Icon;
iconVariant?: 'Bold' | 'Outline';
iconImage?: string;
type?: 'filled' | 'outlined' | 'menu';
type?: ButtonType;
}
const ButtonBase: React.FC<ButtonProps> = ({
@@ -128,7 +130,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
/>
)}
{iconImage && <Image source={{ uri: iconImage }} style={styles.icon} />}
{title && <Text style={styles.text}>{title}</Text>}
{title && <Text style={styles.text} selectable={false}>{title}</Text>}
</View>
)}
</InteractiveBase>

View File

@@ -0,0 +1,27 @@
import { BlurView } from 'expo-blur';
import { ReactNode } from 'react';
import React from 'react';
import { StyleProp, ViewStyle } from 'react-native';
import useColorScheme from '../../hooks/colorScheme';
type GlassmorphismCCProps = {
children?: ReactNode,
style?: StyleProp<ViewStyle>;
};
const GlassmorphismCC = ({ children, style }: GlassmorphismCCProps) => {
const colorScheme = useColorScheme();
console.log(colorScheme);
return (
<BlurView
style={[{borderRadius: 12}, style]}
intensity={70}
tint={colorScheme === 'light' ? 'light' : 'dark'}
>
{children}
</BlurView>
);
};
export default GlassmorphismCC;

View File

@@ -0,0 +1,64 @@
import { Text, Row, Heading, Column, Center } from 'native-base';
import ButtonBase, { ButtonType } from './ButtonBase';
import { CloseSquare, LoginCurve, LogoutCurve } from 'iconsax-react-native';
import { useDispatch } from 'react-redux';
import { translate } from '../../i18n/i18n';
import { unsetAccessToken } from '../../state/UserSlice';
import { BlurView } from 'expo-blur';
import { useState } from 'react';
import Modal from "react-native-modal";
import React from 'react';
import SignUpForm from '../../components/forms/signupform';
import API, { APIError } from '../../API';
import PopupCC from './PopupCC';
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('loggedIn');
};
type LogoutButtonCCProps = {
isGuest?: boolean;
style: any;
buttonType: ButtonType
};
const LogoutButtonCC = ({isGuest = false, buttonType = 'menu', style}: LogoutButtonCCProps) => {
const dispatch = useDispatch();
const [isVisible, setIsVisible] = useState(false);
return (
<>
<ButtonBase
style={style}
icon={LogoutCurve}
title={translate('signOutBtn')}
type={buttonType}
onPress={async () => {isGuest ? setIsVisible(true) : dispatch(unsetAccessToken());}}
/>
<PopupCC
title={translate('Attention')}
description={translate('transformGuestToUserExplanations')}
isVisible={isVisible}
setIsVisible={setIsVisible}
>
<SignUpForm onSubmit={handleSubmit} />
<ButtonBase
style={{width: '100%'}}
type="outlined"
icon={LogoutCurve}
title={translate('signOutBtn')}
onPress={async () => { dispatch(unsetAccessToken()) }}
/>
</PopupCC>
</>
);
};
export default LogoutButtonCC;

View File

@@ -0,0 +1,62 @@
import { Text, Row, Heading, Column } from 'native-base';
import ButtonBase from './ButtonBase';
import { CloseSquare } from 'iconsax-react-native';
import { BlurView } from 'expo-blur';
import { ReactNode } from 'react';
import Modal from "react-native-modal";
import React from 'react';
import GlassmorphismCC from './Glassmorphism';
type PopupCCProps = {
title: string,
description?: string,
children?: ReactNode,
isVisible: boolean,
setIsVisible: (isVisible: boolean) => void,
};
const PopupCC = ({ title, description, children, isVisible, setIsVisible }: PopupCCProps) => {
return (
<Modal
backdropOpacity={0.3}
isVisible={isVisible}
style={{
display: 'flex',
alignContent: 'center',
alignSelf: 'center',
alignItems: 'center'
}}
>
<GlassmorphismCC>
<Column
style={{
maxWidth: '800px',
maxHeight: 'fit-content',
padding: '20px',
}}
space={4}
>
<Heading size="md" mb={2} alignItems={'flex-end'}>
<Row style={{flex: 1, width: '100%', alignItems: 'flex-end'}}>
<Text style={{flex: 1,width: '100%'}}>
{title}
</Text>
<ButtonBase
type='menu'
style={{width: 'fit-content'}}
icon={CloseSquare}
onPress={async () => setIsVisible(false)}
/>
</Row>
</Heading>
{description &&
<Text>{description}</Text>
}
{children}
</Column>
</GlassmorphismCC>
</Modal>
);
};
export default PopupCC;

View File

@@ -1,13 +1,22 @@
import { LinearGradient } from 'expo-linear-gradient';
import { Center, Flex, Stack, View, Text, Wrap, Image } from 'native-base';
import { Flex, Stack, View, Text, Wrap, Image, Row, Column, ScrollView, useToast } from 'native-base';
import { FunctionComponent } from 'react';
import { Linking, useWindowDimensions } from 'react-native';
import ButtonBase from './ButtonBase';
import { translate } from '../../i18n/i18n';
import API from '../../API';
import API, { APIError } from '../../API';
import SeparatorBase from './SeparatorBase';
import LinkBase from './LinkBase';
import ImageBanner from '../../assets/banner.jpg';
import { useDispatch } from '../../state/Store';
import { setAccessToken } from '../../state/UserSlice';
import useColorScheme from '../../hooks/colorScheme';
const handleGuestLogin = async (apiSetter: (accessToken: string) => void): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount();
apiSetter(apiAccess);
return translate('loggedIn');
};
interface ScaffoldAuthProps {
title: string;
@@ -25,6 +34,12 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
link,
}) => {
const layout = useWindowDimensions();
const dispatch = useDispatch();
const toast = useToast();
const colorScheme = useColorScheme();
const logo = colorScheme == 'light'
? require('../../assets/icon_light.png')
: require('../../assets/icon_dark.png');
return (
<Flex
@@ -32,54 +47,89 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
justifyContent="space-between"
style={{ flex: 1, backgroundColor: '#101014' }}
>
<Center style={{ flex: 1 }}>
<View style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Stack
space={8}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%', paddingBottom: 40 }}
>
<Text fontSize="4xl" textAlign="center">
{title}
</Text>
<Text fontSize="lg" textAlign="center">
{description}
</Text>
</Stack>
<Stack
space={5}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%' }}
>
<ButtonBase
style={{ width: '100%' }}
type="outlined"
iconImage="https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png"
title={translate('continuewithgoogle')}
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
<Column style={{ flex: 1 }}>
<Wrap space={4} direction='row' style={{padding: 16, paddingBottom: 0}}>
<Row space={2} flex={1}>
<Image
source={{ uri: logo }}
style={{
aspectRatio: 1,
width: '32px',
height: '32px',
}}
/>
<SeparatorBase>or</SeparatorBase>
{layout.width > 650 &&
<Text fontSize={'xl'} selectable={false}>
Chromacase
</Text>
}
</Row>
<ButtonBase
title='guest mode'
onPress={async () => {
try {
handleGuestLogin((accessToken: string) => {
dispatch(setAccessToken(accessToken));
});
} catch (error) {
if (error instanceof APIError) {
toast.show({ description: translate(error.userMessage) });
return;
}
toast.show({ description: error as string });
}
}}
/>
</Wrap>
<ScrollView contentContainerStyle={{ padding: 16, flexGrow: 1, justifyContent: 'center', alignSelf: 'center' }}>
<View style={{ width: '100%', maxWidth: 420 }}>
<Stack
space={3}
space={8}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%', paddingBottom: 40 }}
>
<Text fontSize="4xl" textAlign="center">
{title}
</Text>
<Text fontSize="lg" textAlign="center">
{description}
</Text>
</Stack>
<Stack
space={5}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%' }}
>
{form}
<ButtonBase
style={{ width: '100%' }}
type="outlined"
iconImage="https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png"
title={translate('continuewithgoogle')}
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/>
<SeparatorBase>or</SeparatorBase>
<Stack
space={3}
justifyContent="center"
alignContent="center"
alignItems="center"
style={{ width: '100%' }}
>
{form}
</Stack>
{submitButton}
<Wrap style={{ flexDirection: 'row', justifyContent: 'center' }}>
<Text>{link.description}</Text>
<LinkBase onPress={link.onPress}>{link.text}</LinkBase>
</Wrap>
</Stack>
{submitButton}
<Wrap style={{ flexDirection: 'row', justifyContent: 'center' }}>
<Text>{link.description}</Text>
<LinkBase onPress={link.onPress}>{link.text}</LinkBase>
</Wrap>
</Stack>
</View>
</Center>
</View>
</ScrollView>
</Column>
{layout.width > 650 ? (
<View style={{ width: '50%', height: '100%', padding: 16 }}>
<Image

View File

@@ -10,7 +10,6 @@ import {
Cup,
Discover,
Icon,
LogoutCurve,
Music,
SearchNormal1,
Setting2,
@@ -42,9 +41,10 @@ const menu: {
type ScaffoldCCProps = {
children?: React.ReactNode;
routeName: string;
withPadding?: boolean;
};
const ScaffoldCC = (props: ScaffoldCCProps) => {
const ScaffoldCC = ({children, routeName, withPadding = true}: ScaffoldCCProps) => {
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const userQuery = useQuery(API.getUserInfo);
@@ -53,19 +53,20 @@ const ScaffoldCC = (props: ScaffoldCCProps) => {
return <LoadingView />;
}
const colorScheme = useColorScheme();
const logo = colorScheme == 'light'
? require('../../assets/icon_light.png')
: require('../../assets/icon_dark.png');
if (screenSize === 'small') {
return (
<ScaffoldMobileCC
user={userQuery.data}
logo={colorScheme == 'light'
? require('../../assets/icon_light.png')
: require('../../assets/icon_dark.png')}
routeName={props.routeName}
logo={logo}
routeName={routeName}
menu={menu}
>
{props.children}
{children}
</ScaffoldMobileCC>
);
}
@@ -73,13 +74,12 @@ const ScaffoldCC = (props: ScaffoldCCProps) => {
return (
<ScaffoldDesktopCC
user={userQuery.data}
logo={colorScheme == 'light'
? require('../../assets/icon_light.png')
: require('../../assets/icon_dark.png')}
routeName={props.routeName}
logo={logo}
routeName={routeName}
menu={menu}
widthPadding={withPadding}
>
{props.children}
{children}
</ScaffoldDesktopCC>
);
};

View File

@@ -1,20 +1,21 @@
import { View, Image } from 'react-native';
import { Divider, Text, ScrollView, Flex, Row, Popover, Heading, Button } from 'native-base';
import { Divider, Text, ScrollView, Flex, Row } from 'native-base';
import { useQuery, useQueries } from '../../Queries';
import API from '../../API';
import Song from '../../models/Song';
import { LinearGradient } from 'expo-linear-gradient';
import ButtonBase from './ButtonBase';
import { Icon, LogoutCurve } from 'iconsax-react-native';
import { useDispatch } from 'react-redux';
import { Icon } from 'iconsax-react-native';
import { LoadingView } from '../Loading';
import { translate } from '../../i18n/i18n';
import { unsetAccessToken } from '../../state/UserSlice';
import { useNavigation } from '../../Navigation';
import Spacer from './Spacer';
import User from '../../models/User';
import LogoutButtonCC from './LogoutButtonCC';
import GlassmorphismCC from './Glassmorphism';
type ScaffoldDesktopCCProps = {
widthPadding: boolean,
children?: React.ReactNode;
user: User;
logo: string;
@@ -30,7 +31,6 @@ type ScaffoldDesktopCCProps = {
const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
const navigation = useNavigation();
const userQuery = useQuery(API.getUserInfo);
const dispatch = useDispatch();
if (!userQuery.data || userQuery.isLoading) {
return <LoadingView />;
@@ -160,75 +160,25 @@ const ScaffoldDesktopCC = (props: ScaffoldDesktopCCProps) => {
/>
))}
<Spacer />
{!props.user.isGuest && (
<ButtonBase
style={{ width: '100%' }}
icon={LogoutCurve}
title={translate('signOutBtn')}
type="menu"
onPress={async () => {
dispatch(unsetAccessToken());
}}
/>
)}
{props.user.isGuest && (
<Popover
trigger={(triggerProps) => (
<ButtonBase {...triggerProps}>
{translate('signOutBtn')}
</ButtonBase>
)}
>
<Popover.Content>
<Popover.Arrow />
<Popover.Body>
<Heading size="md" mb={2}>
{translate('Attention')}
</Heading>
<Text>
{translate(
'YouAreCurrentlyConnectedWithAGuestAccountWarning'
)}
</Text>
<Button.Group variant="ghost" space={2}>
<Button
onPress={() => dispatch(unsetAccessToken())}
colorScheme="red"
>
{translate('signOutBtn')}
</Button>
<Button
onPress={() => {
navigation.navigate('Login');
}}
colorScheme="green"
>
{translate('signUpBtn')}
</Button>
</Button.Group>
</Popover.Body>
</Popover.Content>
</Popover>
)}
<LogoutButtonCC isGuest={props.user.isGuest} style={{with: '100%'}} buttonType={'menu'}/>
</View>
</View>
<ScrollView
style={{ flex: 1, maxHeight: '100vh' }}
contentContainerStyle={{ flex: 1 }}
>
<View
<GlassmorphismCC
style={{
backgroundColor: 'rgba(16,16,20,0.5)',
flex: 1,
margin: 8,
padding: 20,
padding: props.widthPadding ? 20 : 0,
borderRadius: 12,
minHeight: 'fit-content',
}}
>
{props.children}
</View>
</GlassmorphismCC>
<Spacer/>
</ScrollView>
</View>

View File

@@ -1,10 +1,13 @@
// a form for sign up
import React from 'react';
import { Translate, translate } from '../../i18n/i18n';
import { translate } from '../../i18n/i18n';
import { string } from 'yup';
import { FormControl, Input, Stack, WarningOutlineIcon, Box, useToast } from 'native-base';
import TextButton from '../TextButton';
import { useToast, Column } from 'native-base';
import TextFormField from '../UI/TextFormField';
import { Lock1, Sms, User } from 'iconsax-react-native';
import ButtonBase from '../UI/ButtonBase';
import Spacer from '../UI/Spacer';
interface SignupFormProps {
onSubmit: (username: string, password: string, email: string) => Promise<string>;
@@ -29,7 +32,6 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
error: null as string | null,
},
});
const [submittingForm, setSubmittingForm] = React.useState(false);
const validationSchemas = {
username: string()
@@ -51,144 +53,111 @@ const SignUpForm = ({ onSubmit }: SignupFormProps) => {
const toast = useToast();
return (
<Box alignItems="center" style={{ width: '100%' }}>
<Box style={{ width: '80%', maxWidth: 400 }}>
<Stack mx="4">
<FormControl
isRequired
isInvalid={
formData.username.error !== null ||
formData.password.error !== null ||
formData.repeatPassword.error !== null ||
formData.email.error !== null
}
>
<FormControl.Label>
<Translate translationKey="username" />
</FormControl.Label>
<Input
isRequired
type="text"
placeholder="Katerina"
autoComplete="username-new"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey="email" />
</FormControl.Label>
<Input
isRequired
type="text"
placeholder="lucy@er.com"
autoComplete="email"
value={formData.email.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.email
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, email: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.email.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey="password" />
</FormControl.Label>
<Input
isRequired
type="password"
autoComplete="password-new"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.password.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey="repeatPassword" />
</FormControl.Label>
<Input
isRequired
type="password"
autoComplete="password-new"
value={formData.repeatPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
if (!error && t !== formData.password.value) {
error = translate('passwordsDontMatch');
}
setFormData({
...formData,
repeatPassword: { value: t, error },
});
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.repeatPassword.error}
</FormControl.ErrorMessage>
<TextButton
translate={{ translationKey: 'signUpBtn' }}
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
formData.repeatPassword.error !== null ||
formData.email.error !== null ||
formData.username.value === '' ||
formData.password.value === '' ||
formData.repeatPassword.value === '' ||
formData.repeatPassword.value === ''
<Column space={4}>
<TextFormField
isRequired
icon={User}
error={formData.username.error}
placeholder="Username"
autoComplete="username-new"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
/>
<TextFormField
isRequired
icon={Sms}
error={formData.email.error}
placeholder="Email"
autoComplete="email"
value={formData.email.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.email
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, email: { value: t, error } });
});
}}
/>
<TextFormField
isRequired
isSecret
icon={Lock1}
error={formData.password.error}
placeholder="Password"
autoComplete="password-new"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
/>
<TextFormField
isRequired
isSecret
error={formData.repeatPassword.error}
icon={Lock1}
placeholder="Repeat password"
autoComplete="password-new"
value={formData.repeatPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
if (!error && t !== formData.password.value) {
error = translate('passwordsDontMatch');
}
onPress={async () => {
setSubmittingForm(true);
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value,
formData.email.value
);
toast.show({ description: resp });
setSubmittingForm(false);
} catch (e) {
toast.show({ description: e as string });
setSubmittingForm(false);
}
}}
/>
</FormControl>
</Stack>
</Box>
</Box>
setFormData({
...formData,
repeatPassword: { value: t, error },
});
});
}}
/>
<Spacer height='xs'/>
<ButtonBase
style={{ width: '100%' }}
title={translate('signUpBtn')}
isDisabled={
formData.username.error !== null ||
formData.email.error !== null ||
formData.password.error !== null ||
formData.repeatPassword.error !== null ||
formData.username.value === '' ||
formData.email.value === '' ||
formData.password.value === '' ||
formData.repeatPassword.value === ''
}
onPress={async () => {
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value,
formData.email.value
);
toast.show({ description: resp });
} catch (e) {
toast.show({ description: e as string });
}
}}
/>
</Column>
);
};

View File

@@ -149,7 +149,7 @@ export const en = {
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.',
'You are currently logged in with a guest account. Logging out will result in the loss of your data. To save your progress, please register.',
SettingsNotificationsPushNotifications: 'Push',
SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsTrainingReminder: 'Training reminder',
@@ -334,7 +334,7 @@ export const fr: typeof en = {
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.",
"Vous êtes actuellement connecté avec un compte invité. La déconnexion entraînera la perte de vos données. Pour sauvegarder votre progression, veuillez vous inscrire.",
SettingsCategoryGuest: 'Invité',
SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsPushNotifications: 'Notifications push',
@@ -523,7 +523,7 @@ export const sp: typeof en = {
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.',
'Actualmente está conectado con una cuenta de invitado. Si cierra la sesión, perderá sus datos. Para guardar su progreso, regístrese.',
SettingsCategoryGuest: 'Invitado',
SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsPushNotifications: 'Notificaciones push',

View File

@@ -34,6 +34,7 @@
"add": "^2.0.6",
"expo": "^47.0.8",
"expo-asset": "~8.7.0",
"expo-blur": "~12.0.1",
"expo-dev-client": "~2.0.1",
"expo-image-picker": "~14.0.2",
"expo-linear-gradient": "~12.0.1",
@@ -59,6 +60,7 @@
"react-i18next": "^11.18.3",
"react-native": "0.70.5",
"react-native-chart-kit": "^6.12.0",
"react-native-modal": "^13.0.1",
"react-native-paper": "^4.12.5",
"react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1",

View File

@@ -3,7 +3,7 @@ import { useQueries, useQuery } from '../Queries';
import API from '../API';
import { LoadingView } from '../components/Loading';
import { Box, Flex, Stack, Heading, VStack, HStack } from 'native-base';
import { useNavigation } from '../Navigation';
import { RouteProps, useNavigation } from '../Navigation';
import SongCardGrid from '../components/SongCardGrid';
import CompetenciesTable from '../components/CompetenciesTable';
import ProgressBar from '../components/ProgressBar';
@@ -11,8 +11,9 @@ import Translate from '../components/Translate';
import TextButton from '../components/TextButton';
import Song from '../models/Song';
import { FontAwesome5 } from '@expo/vector-icons';
import ScaffoldCC from '../components/UI/ScaffoldCC';
const HomeView = () => {
const HomeView = (props: RouteProps<{}>) => {
const navigation = useNavigation();
const userQuery = useQuery(API.getUserInfo);
const playHistoryQuery = useQuery(API.getUserPlayHistory);
@@ -39,7 +40,7 @@ const HomeView = () => {
return <LoadingView />;
}
return (
<Flex>
<ScaffoldCC routeName={props.route.name}>
<Flex>
<Stack
space={4}
@@ -182,7 +183,7 @@ const HomeView = () => {
</Box>
</VStack>
</Stack>
</Flex>
</ScaffoldCC>
);
};

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { useWindowDimensions } from 'react-native';
import { Column, Flex, Progress, Row, Text, Wrap } from 'native-base';
import { useNavigation } from '../Navigation';
import { RouteProps, useNavigation } from '../Navigation';
import UserAvatar from '../components/UserAvatar';
import { LoadingView } from '../components/Loading';
import { useQuery } from '../Queries';
@@ -9,6 +9,7 @@ import API from '../API';
import ButtonBase from '../components/UI/ButtonBase';
import { translate } from '../i18n/i18n';
import ScoreGraph from '../components/ScoreGraph';
import ScaffoldCC from '../components/UI/ScaffoldCC';
const fakeData = [
{
@@ -721,7 +722,7 @@ function xpToProgressBarValue(xp: number): number {
return Math.floor(xp / 10);
}
const ProfileView = () => {
const ProfileView = (props: RouteProps<{}>) => {
const layout = useWindowDimensions();
const navigation = useNavigation();
const userQuery = useQuery(API.getUserInfo);
@@ -736,69 +737,71 @@ const ProfileView = () => {
const level = xpToLevel(userQuery.data.data.xp);
return (
<Flex>
<Wrap
style={{
flexDirection: layout.width > 650 ? 'row' : 'column',
alignItems: 'center',
paddingBottom: 20,
justifyContent: 'space-between',
}}
>
<UserAvatar size={layout.width > 650 ? 'xl' : '2xl'} />
<Column
<ScaffoldCC routeName={props.route.name}>
<Flex>
<Wrap
style={{
paddingLeft: layout.width > 650 ? 20 : 0,
paddingTop: layout.width > 650 ? 0 : 20,
flex: 1,
width: '100%',
flexDirection: layout.width > 650 ? 'row' : 'column',
alignItems: 'center',
paddingBottom: 20,
justifyContent: 'space-between',
}}
>
<Wrap
<UserAvatar size={layout.width > 650 ? 'xl' : '2xl'} />
<Column
style={{
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 20,
justifyContent: 'space-between',
paddingLeft: layout.width > 650 ? 20 : 0,
paddingTop: layout.width > 650 ? 0 : 20,
flex: 1,
width: '100%',
}}
>
<Text fontSize={'xl'} style={{ paddingRight: 'auto' }}>
{userQuery.data.name}
<Wrap
style={{
flexDirection: 'row',
alignItems: 'center',
paddingBottom: 20,
justifyContent: 'space-between',
}}
>
<Text fontSize={'xl'} style={{ paddingRight: 'auto' }}>
{userQuery.data.name}
</Text>
<ButtonBase
title="Modifier profil"
style={{ width: 'fit-content' }}
type={'filled'}
onPress={async () => navigation.navigate('Settings')}
/>
</Wrap>
<Text style={{ paddingBottom: 10, fontWeight: 'bold' }}>
Account created on {userQuery.data.data.createdAt.toLocaleDateString()}
</Text>
<ButtonBase
title="Modifier profil"
style={{ width: 'fit-content' }}
type={'filled'}
onPress={async () => navigation.navigate('Settings')}
/>
</Wrap>
<Text style={{ paddingBottom: 10, fontWeight: 'bold' }}>
Account created on {userQuery.data.data.createdAt.toLocaleDateString()}
</Text>
<Wrap style={{ flexDirection: 'row', alignItems: 'center', paddingBottom: 10 }}>
<Text style={{ paddingRight: 20 }}>
Your client ID is {userQuery.data.id}
</Text>
<Text>{userQuery.data.data.gamesPlayed} Games played</Text>
</Wrap>
</Column>
</Wrap>
<Row style={{ alignItems: 'center', paddingBottom: 20 }}>
<Text style={{ paddingRight: 20 }}>{`${translate('level')} ${level}`}</Text>
<Progress
bgColor={'#rgba(16,16,20,0.5)'}
value={progessValue}
w={'2/3'}
maxW={'400'}
<Wrap style={{ flexDirection: 'row', alignItems: 'center', paddingBottom: 10 }}>
<Text style={{ paddingRight: 20 }}>
Your client ID is {userQuery.data.id}
</Text>
<Text>{userQuery.data.data.gamesPlayed} Games played</Text>
</Wrap>
</Column>
</Wrap>
<Row style={{ alignItems: 'center', paddingBottom: 20 }}>
<Text style={{ paddingRight: 20 }}>{`${translate('level')} ${level}`}</Text>
<Progress
bgColor={'#rgba(16,16,20,0.5)'}
value={progessValue}
w={'2/3'}
maxW={'400'}
/>
</Row>
<ScoreGraph
songHistory={{
history: fakeData,
best: 200,
}}
/>
</Row>
<ScoreGraph
songHistory={{
history: fakeData,
best: 200,
}}
/>
</Flex>
</Flex>
</ScaffoldCC>
);
};

View File

@@ -11,6 +11,7 @@ import { Filter } from '../components/SearchBar';
import { ScrollView } from 'native-base';
import { RouteProps } from '../Navigation';
import LikedSong from '../models/LikedSong';
import ScaffoldCC from '../components/UI/ScaffoldCC';
interface SearchContextType {
filter: 'artist' | 'song' | 'genre' | 'all' | 'favorites';
@@ -81,30 +82,32 @@ const SearchView = (props: RouteProps<SearchViewProps>) => {
};
return (
<ScrollView>
<SafeAreaView>
<SearchContext.Provider
value={{
filter,
stringQuery,
songData,
artistData,
genreData,
favoriteData,
isLoadingSong,
isLoadingArtist,
isLoadingGenre,
isLoadingFavorite,
updateFilter,
updateStringQuery,
}}
>
<SearchBar />
<SearchResultComponent />
</SearchContext.Provider>
</SafeAreaView>
</ScrollView>
<ScaffoldCC routeName={props.route.name}>
<ScrollView>
<SafeAreaView>
<SearchContext.Provider
value={{
filter,
stringQuery,
songData,
artistData,
genreData,
favoriteData,
isLoadingSong,
isLoadingArtist,
isLoadingGenre,
isLoadingFavorite,
updateFilter,
updateStringQuery,
}}
>
<SearchBar />
<SearchResultComponent />
</SearchContext.Provider>
</SafeAreaView>
</ScrollView>
</ScaffoldCC>
);
};
export default SearchView;
export default SearchView;

View File

@@ -163,15 +163,15 @@ const SignupView = () => {
submitButton={
<ButtonBase
style={{ width: '100%' }}
title="Signin"
title={translate('signUpBtn')}
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
formData.repeatPassword.error !== null ||
formData.email.error !== null ||
formData.password.error !== null ||
formData.repeatPassword.error !== null ||
formData.username.value === '' ||
formData.email.value === '' ||
formData.password.value === '' ||
formData.repeatPassword.value === '' ||
formData.repeatPassword.value === ''
}
onPress={async () => {

View File

@@ -5,7 +5,8 @@ import { useQuery, useQueries } from '../../Queries';
import HomeMainSongCard from '../../components/V2/HomeMainSongCard';
import SongCardInfo from '../../components/V2/SongCardInfo';
import API from '../../API';
import { useNavigation } from '../../Navigation';
import { RouteProps, useNavigation } from '../../Navigation';
import ScaffoldCC from '../../components/UI/ScaffoldCC';
const bigSideRatio = 1000;
const smallSideRatio = 618;
@@ -45,7 +46,7 @@ const cards = [
},
] as [HomeCardProps, HomeCardProps, HomeCardProps, HomeCardProps];
const HomeView = () => {
const HomeView = (props: RouteProps<{}>) => {
const songsQuery = useQuery(API.getSongSuggestions);
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isPhone = screenSize === 'small';
@@ -76,36 +77,22 @@ const HomeView = () => {
}, [artistsQueries]);
return (
<View
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<View>
<View
style={{
alignSelf: 'stretch',
maxWidth: '1100px',
alignItems: 'stretch',
flexDirection: isPhone ? 'column' : 'row',
}}
>
<ScaffoldCC routeName={props.route.name}>
<View
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
}}
>
<View>
<View
style={{
flexGrow: bigSideRatio,
}}
>
<HomeMainSongCard {...cards[0]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'row' : 'column',
alignSelf: 'stretch',
maxWidth: '1100px',
alignItems: 'stretch',
flexDirection: isPhone ? 'column' : 'row',
}}
>
<View
@@ -113,13 +100,13 @@ const HomeView = () => {
flexGrow: bigSideRatio,
}}
>
<HomeMainSongCard {...cards[1]} />
<HomeMainSongCard {...cards[0]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'column-reverse' : 'row-reverse',
flexDirection: isPhone ? 'row' : 'column',
alignItems: 'stretch',
}}
>
@@ -128,83 +115,99 @@ const HomeView = () => {
flexGrow: bigSideRatio,
}}
>
<HomeMainSongCard {...cards[2]} />
<HomeMainSongCard {...cards[1]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'row-reverse' : 'column-reverse',
flexDirection: isPhone ? 'column-reverse' : 'row-reverse',
alignItems: 'stretch',
}}
>
<View
style={{
flexGrow: bigSideRatio,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'flex-end',
}}
>
<HomeMainSongCard {...cards[3]} />
<HomeMainSongCard {...cards[2]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
display: 'flex',
flexDirection: isPhone ? 'row-reverse' : 'column-reverse',
alignItems: 'stretch',
}}
></View>
>
<View
style={{
flexGrow: bigSideRatio,
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
justifyContent: 'flex-end',
}}
>
<HomeMainSongCard {...cards[3]} />
</View>
<View
style={{
flexGrow: smallSideRatio,
}}
></View>
</View>
</View>
</View>
</View>
</View>
</View>
<View
style={{
flexShrink: 0,
flexGrow: 0,
flexBasis: '15%',
width: '100%',
}}
>
<Text
style={{
color: 'white',
fontSize: 24,
fontWeight: 'bold',
marginLeft: 16,
marginBottom: 16,
marginTop: 24,
}}
>
{'Suggestions'}
</Text>
{songsQuery.isLoading && <Text>Loading...</Text>}
<View
style={{
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-start',
alignItems: 'flex-start',
// @ts-expect-error - gap is not in the typings
gap: 16,
flexShrink: 0,
flexGrow: 0,
flexBasis: '15%',
width: '100%',
}}
>
{songsQuery.data?.map((song) => (
<SongCardInfo
key={song.id}
song={song}
onPress={() => {
navigation.navigate('Song', { songId: song.id });
}}
onPlay={() => {
console.log('play');
}}
/>
))}
<Text
style={{
color: 'white',
fontSize: 24,
fontWeight: 'bold',
marginLeft: 16,
marginBottom: 16,
marginTop: 24,
}}
>
{'Suggestions'}
</Text>
{songsQuery.isLoading && <Text>Loading...</Text>}
<View
style={{
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'flex-start',
alignItems: 'flex-start',
// @ts-expect-error - gap is not in the typings
gap: 16,
}}
>
{songsQuery.data?.map((song) => (
<SongCardInfo
key={song.id}
song={song}
onPress={() => {
navigation.navigate('Song', { songId: song.id });
}}
onPlay={() => {
console.log('play');
}}
/>
))}
</View>
</View>
</View>
</View>
</ScaffoldCC>
);
};

View File

@@ -1,36 +0,0 @@
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) => {
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('loggedIn');
};
const GuestToUserView = () => {
return (
<Center flex={1} justifyContent={'center'}>
<Center width="90%" justifyContent={'center'}>
<Heading>{translate('signUp')}</Heading>
<Text mt={5} mb={10}>
{translate('transformGuestToUserExplanations')}
</Text>
<SignUpForm
onSubmit={(username, password, email) =>
handleSubmit(username, password, email)
}
/>
</Center>
</Center>
);
};
export default GuestToUserView;

View File

@@ -1,100 +0,0 @@
import React from 'react';
import { Flex } from 'native-base';
import { translate } from '../../i18n/i18n';
import ElementList from '../../components/GtkUI/ElementList';
import useUserSettings from '../../hooks/userSettings';
import { LoadingView } from '../../components/Loading';
import { Calendar1, MonitorMobbile, Send2, Warning2 } from 'iconsax-react-native';
const NotificationsView = () => {
const { settings, updateSettings } = useUserSettings();
if (!settings.data) {
return <LoadingView />;
}
return (
<Flex
style={{
flex: 1,
alignItems: 'center',
paddingTop: 32,
}}
>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: 'toggle',
icon: <MonitorMobbile size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsPushNotifications'),
description: 'Cette notification apparaitra sur votre apparail en pop-up',
data: {
value: settings.data.notifications.pushNotif,
onToggle: () => {
updateSettings({
notifications: {
pushNotif: !settings.data.notifications.pushNotif,
},
});
},
},
},
{
type: 'toggle',
icon: <Send2 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsEmailNotifications'),
description: 'Recevez des mails pour atteindre vos objectifs',
data: {
value: settings.data.notifications.emailNotif,
onToggle: () => {
updateSettings({
notifications: {
emailNotif: !settings.data.notifications.emailNotif,
},
});
},
},
},
{
type: 'toggle',
icon: <Calendar1 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsTrainingReminder'),
description: 'Un apprentissage régulier est la clé',
data: {
value: settings.data.notifications.trainNotif,
onToggle: () => {
updateSettings({
notifications: {
trainNotif: !settings.data.notifications.trainNotif,
},
});
},
},
},
{
type: 'toggle',
icon: <Warning2 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsReleaseAlert'),
description: 'Restez informé de nos mises à jour',
data: {
value: settings.data.notifications.newSongNotif,
onToggle: () => {
updateSettings({
notifications: {
newSongNotif: !settings.data.notifications.newSongNotif,
},
});
},
},
},
]}
/>
</Flex>
);
};
export default NotificationsView;

View File

@@ -0,0 +1,88 @@
import React from 'react';
import { Flex } from 'native-base';
import { translate } from '../../i18n/i18n';
import ElementList from '../../components/GtkUI/ElementList';
import useUserSettings from '../../hooks/userSettings';
import { LoadingView } from '../../components/Loading';
import { Calendar1, MonitorMobbile, Send2, Warning2 } from 'iconsax-react-native';
const NotificationsSettings = () => {
const { settings, updateSettings } = useUserSettings();
if (!settings.data) {
return <LoadingView />;
}
return (
<ElementList
style={{width: '100%'}}
elements={[
{
type: 'toggle',
icon: <MonitorMobbile size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsPushNotifications'),
description: 'Cette notification apparaitra sur votre apparail en pop-up',
data: {
value: settings.data.notifications.pushNotif,
onToggle: () => {
updateSettings({
notifications: {
pushNotif: !settings.data.notifications.pushNotif,
},
});
},
},
},
{
type: 'toggle',
icon: <Send2 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsEmailNotifications'),
description: 'Recevez des mails pour atteindre vos objectifs',
data: {
value: settings.data.notifications.emailNotif,
onToggle: () => {
updateSettings({
notifications: {
emailNotif: !settings.data.notifications.emailNotif,
},
});
},
},
},
{
type: 'toggle',
icon: <Calendar1 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsTrainingReminder'),
description: 'Un apprentissage régulier est la clé',
data: {
value: settings.data.notifications.trainNotif,
onToggle: () => {
updateSettings({
notifications: {
trainNotif: !settings.data.notifications.trainNotif,
},
});
},
},
},
{
type: 'toggle',
icon: <Warning2 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('SettingsNotificationsReleaseAlert'),
description: 'Restez informé de nos mises à jour',
data: {
value: settings.data.notifications.newSongNotif,
onToggle: () => {
updateSettings({
notifications: {
newSongNotif: !settings.data.notifications.newSongNotif,
},
});
},
},
},
]}
/>
);
};
export default NotificationsSettings;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { Flex } from 'native-base';
import { Column, Flex } from 'native-base';
import { useLanguage } from '../../state/LanguageSlice';
import { AvailableLanguages, DefaultLanguage, translate } from '../../i18n/i18n';
import { useSelector } from '../../state/Store';
@@ -9,24 +9,13 @@ import ElementList from '../../components/GtkUI/ElementList';
import LocalSettings from '../../models/LocalSettings';
import { Brush2, Colorfilter, LanguageSquare, Rank, Sound } from 'iconsax-react-native';
const PreferencesView = () => {
const PreferencesSettings = () => {
const dispatch = useDispatch();
const language = useSelector((state) => state.language.value);
const settings = useSelector((state) => state.settings.local);
return (
<Flex
style={{
flex: 1,
alignItems: 'center',
paddingTop: 32,
}}
>
<Column space={4} style={{width: '100%'}}>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
icon: <Brush2 size="24" color="#FFF" style={{ minWidth: 24 }} />,
@@ -93,11 +82,6 @@ const PreferencesView = () => {
]}
/>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
icon: <Colorfilter size="24" color="#FFF" style={{ minWidth: 24 }} />,
@@ -114,11 +98,6 @@ const PreferencesView = () => {
]}
/>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
icon: <Sound size="24" color="#FFF" style={{ minWidth: 24 }} />,
@@ -153,8 +132,8 @@ const PreferencesView = () => {
},*/
]}
/>
</Flex>
</Column>
);
};
export default PreferencesView;
export default PreferencesSettings;

View File

@@ -0,0 +1,67 @@
import React from 'react';
import { Flex } 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 { Driver, Like1, Shop } from 'iconsax-react-native';
const PrivacySettings = () => {
const dispatch = useDispatch();
const settings = useSelector((state: RootState) => state.settings.local);
const { settings: userSettings, updateSettings: updateUserSettings } = useUserSettings();
if (!userSettings.data) {
return <LoadingView />;
}
return (
<ElementList
style={{width: '100%'}}
elements={[
{
type: 'toggle',
icon: <Driver size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('dataCollection'),
description:
"Acceptez-vous la récupération de vos données pour l'amélioration de Chromacase ?",
data: {
value: settings.dataCollection,
onToggle: () =>
dispatch(
updateSettings({ dataCollection: !settings.dataCollection })
),
},
},
{
type: 'toggle',
icon: <Shop size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('customAds'),
description: 'Afficher les suggestions dans la section des recommandations',
data: {
value: settings.customAds,
onToggle: () =>
dispatch(updateSettings({ customAds: !settings.customAds })),
},
},
{
type: 'toggle',
icon: <Like1 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('recommendations'),
description: 'Souhaitez-vous recevoir nos conseils et recommandations ?',
data: {
value: userSettings.data.recommendations,
onToggle: () =>
updateUserSettings({
recommendations: !userSettings.data.recommendations,
}),
},
},
]}
/>
);
};
export default PrivacySettings;

View File

@@ -1,79 +0,0 @@
import React from 'react';
import { Flex } 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 { Driver, Like1, Shop } from 'iconsax-react-native';
const PrivacyView = () => {
const dispatch = useDispatch();
const settings = useSelector((state: RootState) => state.settings.local);
const { settings: userSettings, updateSettings: updateUserSettings } = useUserSettings();
if (!userSettings.data) {
return <LoadingView />;
}
return (
<Flex
style={{
flex: 1,
alignItems: 'center',
paddingTop: 32,
}}
>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: 'toggle',
icon: <Driver size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('dataCollection'),
description:
"Acceptez-vous la récupération de vos données pour l'amélioration de Chromacase ?",
data: {
value: settings.dataCollection,
onToggle: () =>
dispatch(
updateSettings({ dataCollection: !settings.dataCollection })
),
},
},
{
type: 'toggle',
icon: <Shop size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('customAds'),
description: 'Afficher les suggestions dans la section des recommandations',
data: {
value: settings.customAds,
onToggle: () =>
dispatch(updateSettings({ customAds: !settings.customAds })),
},
},
{
type: 'toggle',
icon: <Like1 size="24" color="#FFF" style={{ minWidth: 24 }} />,
title: translate('recommendations'),
description: 'Souhaitez-vous recevoir nos conseils et recommandations ?',
data: {
value: userSettings.data.recommendations,
onToggle: () =>
updateUserSettings({
recommendations: !userSettings.data.recommendations,
}),
},
},
]}
/>
</Flex>
);
};
export default PrivacyView;

View File

@@ -1,264 +0,0 @@
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, Popover, Toast } 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 '../../Queries';
import UserAvatar from '../../components/UserAvatar';
import * as ImagePicker from 'expo-image-picker';
// 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(API.getUserInfo);
const dispatch = useDispatch();
if (!userQuery.data || userQuery.isLoading) {
return <LoadingView />;
}
const user = userQuery.data;
return (
<Flex
style={{
flex: 1,
alignItems: 'center',
paddingTop: 40,
}}
>
<Column
style={{
width: '100%',
alignItems: 'center',
}}
>
<Center>
<UserAvatar size="2xl" />
</Center>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: 'text',
title: translate('email'),
data: {
text: user.email || translate('NoAssociatedEmail'),
onPress: () => {
navigation.navigate('changeEmail');
},
},
},
{
type: 'text',
title: translate('verified'),
data: {
text: user.emailVerified ? 'verified' : 'not verified',
onPress: user.emailVerified
? undefined
: () => API.fetch({ route: '/auth/reverify', method: 'PUT' }),
},
},
{
type: 'text',
title: translate('avatar'),
data: {
text: translate('changeIt'),
onPress: () => {
ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
aspect: [1, 1],
quality: 1,
base64: true,
}).then((result) => {
console.log(result);
const image = result.assets?.at(0);
if (!result.canceled && image) {
API.updateProfileAvatar(image)
.then(() => {
userQuery.refetch();
Toast.show({
description: 'Update successful',
});
})
.catch((e) => {
console.error(e);
Toast.show({ description: 'Update failed' });
});
}
});
},
},
},
]}
/>
<ElementList
style={{
marginTop: 20,
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: 'text',
title: translate('username'),
data: {
text: user.name,
},
},
{
type: 'text',
title: 'ID',
helperText: 'This is your unique ID, be proud of it!',
data: {
text: user.id.toString(),
},
},
{
type: 'text',
title: 'Google Account',
data: {
text: user.googleID ? 'Linked' : 'Not linked',
},
// type: 'custom',
// data: user.googleID
// ? <Button><Text>Unlink</Text></Button>
// : <Button><Text>Link</Text></Button>,
},
{
type: 'text',
title: translate('nbGamesPlayed'),
data: {
text: user.data.gamesPlayed.toString(),
},
},
{
type: 'text',
title: 'XP',
description: translate('XPDescription'),
data: {
text: user.data.xp.toString(),
},
},
{
type: 'text',
title: translate('userCreatedAt'),
helperText:
'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'),
data: {
text: translate(user.premium ? 'yes' : 'no'),
},
},
]}
/>
<Heading fontSize="20" mt="7">
Fonctionnalités premium
</Heading>
<ElementList
style={{
marginTop: 10,
width: '90%',
maxWidth: 850,
}}
elements={[
{
type: 'toggle',
title: 'Piano Magique',
description:
'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é',
disabled: true,
data: {
value: false,
onToggle: () => {},
},
},
{
type: 'dropdown',
title: 'Thème de piano',
disabled: true,
data: {
value: 'default',
onSelect: () => {},
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Catpuccino',
value: 'catpuccino',
},
],
},
},
]}
/>
</Column>
<Box mt={10}>
{!user.isGuest && (
<TextButton
onPress={() => dispatch(unsetAccessToken())}
translate={{
translationKey: 'signOutBtn',
}}
/>
)}
{user.isGuest && (
<Popover
trigger={(triggerProps) => (
<Button {...triggerProps}>{translate('signOutBtn')}</Button>
)}
>
<Popover.Content>
<Popover.Arrow />
<Popover.Body>
<Heading size="md" mb={2}>
{translate('Attention')}
</Heading>
<Text>
{translate('YouAreCurrentlyConnectedWithAGuestAccountWarning')}
</Text>
<Button.Group variant="ghost" space={2}>
<Button
onPress={() => dispatch(unsetAccessToken())}
colorScheme="red"
>
{translate('signOutBtn')}
</Button>
<Button
onPress={() => {
navigation.navigate('GuestToUser');
}}
colorScheme="green"
>
{translate('signUpBtn')}
</Button>
</Button.Group>
</Popover.Body>
</Popover.Content>
</Popover>
)}
</Box>
</Flex>
);
};
export default ProfileSettings;

View File

@@ -0,0 +1,73 @@
import API from '../../API';
import React from 'react';
import { Flex } from 'native-base';
import { LoadingView } from '../../components/Loading';
import ElementList from '../../components/GtkUI/ElementList';
import { translate } from '../../i18n/i18n';
import { useQuery } from '../../Queries';
import { Designtools, Magicpen, Star1 } from 'iconsax-react-native';
// Too painful to infer the settings-only, typed navigator. Gave up
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const PremiumSettings = () => {
const userQuery = useQuery(API.getUserInfo);
if (!userQuery.data || userQuery.isLoading) {
return <LoadingView />;
}
const user = userQuery.data;
return (
<ElementList
style={{width: '100%'}}
elements={[
{
icon: <Star1 size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'text',
title: translate('premiumAccount'),
description:
'Personalisation premium et outils vous permetant de passer au niveau supperieur',
data: {
text: translate(user.premium ? 'yes' : 'no'),
},
},
{
icon: <Magicpen size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'toggle',
title: 'Piano Magique',
description:
'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é',
disabled: true,
data: {
value: false,
onToggle: () => {},
},
},
{
icon: <Designtools size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'dropdown',
title: 'Thème de piano',
description: 'Définissez le theme de votre piano',
disabled: true,
data: {
value: 'default',
onSelect: () => {},
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Catpuccino',
value: 'catpuccino',
},
],
},
},
]}
/>
);
};
export default PremiumSettings;

View File

@@ -1,85 +0,0 @@
import API from '../../API';
import React from 'react';
import { Flex } from 'native-base';
import { LoadingView } from '../../components/Loading';
import ElementList from '../../components/GtkUI/ElementList';
import { translate } from '../../i18n/i18n';
import { useQuery } from '../../Queries';
import { Designtools, Magicpen, Star1 } from 'iconsax-react-native';
// Too painful to infer the settings-only, typed navigator. Gave up
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const PremiumSettings = () => {
const userQuery = useQuery(API.getUserInfo);
if (!userQuery.data || userQuery.isLoading) {
return <LoadingView />;
}
const user = userQuery.data;
return (
<Flex
style={{
flex: 1,
alignItems: 'center',
paddingTop: 32,
}}
>
<ElementList
style={{
marginTop: 10,
width: '90%',
maxWidth: 850,
}}
elements={[
{
icon: <Star1 size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'text',
title: translate('premiumAccount'),
description:
'Personalisation premium et outils vous permetant de passer au niveau supperieur',
data: {
text: translate(user.premium ? 'yes' : 'no'),
},
},
{
icon: <Magicpen size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'toggle',
title: 'Piano Magique',
description:
'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é',
disabled: true,
data: {
value: false,
onToggle: () => {},
},
},
{
icon: <Designtools size="24" color="#FFF" style={{ minWidth: 24 }} />,
type: 'dropdown',
title: 'Thème de piano',
description: 'Définissez le theme de votre piano',
disabled: true,
data: {
value: 'default',
onSelect: () => {},
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Catpuccino',
value: 'catpuccino',
},
],
},
},
]}
/>
</Flex>
);
};
export default PremiumSettings;

View File

@@ -1,6 +1,6 @@
import API from '../../API';
import React from 'react';
import { Flex, Toast } from 'native-base';
import { Column, Flex, Toast } from 'native-base';
import { LoadingView } from '../../components/Loading';
import ElementList from '../../components/GtkUI/ElementList';
import { translate } from '../../i18n/i18n';
@@ -9,6 +9,8 @@ import * as ImagePicker from 'expo-image-picker';
import { Google, PasswordCheck, SmsEdit, UserSquare, Verify } from 'iconsax-react-native';
import ChangeEmailForm from '../../components/forms/changeEmailForm';
import ChangePasswordForm from '../../components/forms/changePasswordForm';
import LogoutButtonCC from '../../components/UI/LogoutButtonCC';
import Spacer from '../../components/UI/Spacer';
const handleChangeEmail = async (newEmail: string): Promise<string> => {
await API.updateUserEmail(newEmail);
@@ -30,19 +32,8 @@ const ProfileSettings = () => {
}
const user = userQuery.data;
return (
<Flex
style={{
flex: 1,
alignItems: 'center',
paddingTop: 32,
}}
>
<Column space={4} style={{width: '100%'}}>
<ElementList
style={{
marginTop: 20,
width: '100%',
maxWidth: 850,
}}
elements={[
{
icon: <Google size="24" color="#FFF" style={{ minWidth: 24 }} />,
@@ -147,7 +138,8 @@ const ProfileSettings = () => {
},
]}
/>
</Flex>
<LogoutButtonCC isGuest={user.isGuest} style={{with: 'fit-content'}} buttonType={'filled'}/>
</Column>
);
};

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { Center, Flex, Text } from 'native-base';
import ProfileSettings from './SettingsProfileView';
import NotificationsView from './NotificationView';
import PrivacyView from './PrivacyView';
import PreferencesView from './PreferencesView';
import ProfileSettings from './SettingsProfile';
import NotificationsSettings from './NotificationsSettings';
import PrivacySettings from './PrivacySettings';
import PreferencesSettings from './PreferencesSettings';
import { useWindowDimensions } from 'react-native';
import {
TabView,
@@ -23,9 +23,11 @@ import {
FolderCross,
} from 'iconsax-react-native';
import { Scene } from 'react-native-tab-view/lib/typescript/src/types';
import PremiumSettings from './SettingsPremiumView';
import PremiumSettings from './SettingsPremium';
import { RouteProps } from '../../Navigation';
import ScaffoldCC from '../../components/UI/ScaffoldCC';
export const PianoSettingsView = () => {
export const PianoSettings = () => {
return (
<Center style={{ flex: 1 }}>
<Text>Global settings for the virtual piano</Text>
@@ -36,10 +38,10 @@ export const PianoSettingsView = () => {
const renderScene = SceneMap({
profile: ProfileSettings,
premium: PremiumSettings,
preferences: PreferencesView,
notifications: NotificationsView,
privacy: PrivacyView,
piano: PianoSettingsView,
preferences: PreferencesSettings,
notifications: NotificationsSettings,
privacy: PrivacySettings,
piano: PianoSettings,
});
const getTabData = (key: string) => {
@@ -61,9 +63,8 @@ const getTabData = (key: string) => {
}
};
const SetttingsNavigator = () => {
const SetttingsNavigator = (props: RouteProps<{}>) => {
const layout = useWindowDimensions();
const [index, setIndex] = React.useState(0);
const [routes] = React.useState<Route[]>([
{ key: 'profile', title: 'Profile' },
@@ -73,7 +74,6 @@ const SetttingsNavigator = () => {
{ key: 'privacy', title: 'Privacy' },
{ key: 'piano', title: 'Piano' },
]);
const renderTabBar = (
props: SceneRendererProps & { navigationState: NavigationState<Route> }
) => (
@@ -110,8 +110,16 @@ const SetttingsNavigator = () => {
);
return (
<Flex style={{ flex: 1 }}>
<ScaffoldCC routeName={props.route.name} withPadding={false}>
<TabView
sceneContainerStyle={{
flex: 1,
alignSelf: 'center',
paddingTop: 32,
padding: 20,
maxWidth: 850,
width: '100%'
}}
style={{ height: 'fit-content' }}
renderTabBar={renderTabBar}
navigationState={{ index, routes }}
@@ -119,7 +127,7 @@ const SetttingsNavigator = () => {
onIndexChange={setIndex}
initialLayout={{ width: layout.width }}
/>
</Flex>
</ScaffoldCC>
);
};

View File

@@ -8814,6 +8814,11 @@ expo-asset@~8.7.0:
path-browserify "^1.0.0"
url-parse "^1.5.9"
expo-blur@~12.0.1:
version "12.0.1"
resolved "https://registry.yarnpkg.com/expo-blur/-/expo-blur-12.0.1.tgz#7aa4186620359acfa976dda84360070b634ffe3d"
integrity sha512-7oF/xRIFJukM4/qL6ejZ4Z/4YcVExvBPsBrz7rGYz6PtgAkWwYFR62+ExZOzTEG4hgoPPmlnt1ncimsk/MYUgQ==
expo-constants@~14.0.0, expo-constants@~14.0.2:
version "14.0.2"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-14.0.2.tgz#2cb1dec8f41a64c2fc5b4eecaf77d7661cad01cc"
@@ -15134,7 +15139,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.8.1:
prop-types@^15.0.0, prop-types@^15.6.0, prop-types@^15.6.2, 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==
@@ -15472,6 +15477,13 @@ react-merge-refs@^1.0.0:
resolved "https://registry.yarnpkg.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz#73d88b892c6c68cbb7a66e0800faa374f4c38b06"
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
react-native-animatable@1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/react-native-animatable/-/react-native-animatable-1.3.3.tgz#a13a4af8258e3bb14d0a9d839917e9bb9274ec8a"
integrity sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==
dependencies:
prop-types "^15.7.2"
react-native-chart-kit@^6.12.0:
version "6.12.0"
resolved "https://registry.yarnpkg.com/react-native-chart-kit/-/react-native-chart-kit-6.12.0.tgz#187a4987a668a85b7e93588c248ed2c33b3a06f6"
@@ -15501,6 +15513,14 @@ react-native-iphone-x-helper@^1.3.1:
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"
integrity sha512-HOf0jzRnq2/aFUcdCJ9w9JGzN3gdEg0zFE4FyYlp4jtidqU03D5X7ZegGKfT1EWteR0gPBGp9ye5T5FvSWi9Yg==
react-native-modal@^13.0.1:
version "13.0.1"
resolved "https://registry.yarnpkg.com/react-native-modal/-/react-native-modal-13.0.1.tgz#691f1e646abb96fa82c1788bf18a16d585da37cd"
integrity sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==
dependencies:
prop-types "^15.6.2"
react-native-animatable "1.3.3"
react-native-paper@^4.12.5:
version "4.12.5"
resolved "https://registry.yarnpkg.com/react-native-paper/-/react-native-paper-4.12.5.tgz#5ea4bbe02d416d17802a199de748700358c11d3a"