Restore guest mode
This commit is contained in:
@@ -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])}
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
27
front/components/UI/Glassmorphism.tsx
Normal file
27
front/components/UI/Glassmorphism.tsx
Normal 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;
|
||||
64
front/components/UI/LogoutButtonCC.tsx
Normal file
64
front/components/UI/LogoutButtonCC.tsx
Normal 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;
|
||||
62
front/components/UI/PopupCC.tsx
Normal file
62
front/components/UI/PopupCC.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
88
front/views/settings/NotificationsSettings.tsx
Normal file
88
front/views/settings/NotificationsSettings.tsx
Normal 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;
|
||||
@@ -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;
|
||||
67
front/views/settings/PrivacySettings.tsx
Normal file
67
front/views/settings/PrivacySettings.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
73
front/views/settings/SettingsPremium.tsx
Normal file
73
front/views/settings/SettingsPremium.tsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user