redesign AuthenticationView

This commit is contained in:
mathysPaul
2023-09-20 10:27:24 +02:00
parent 6768b0b2a6
commit 973f9bf5b3
14 changed files with 624 additions and 166 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 AuthenticationView from './views/AuthenticationView';
import StartPageView from './views/StartPageView';
import HomeView from './views/HomeView';
import SearchView from './views/SearchView';
@@ -31,6 +30,8 @@ import ErrorView from './views/ErrorView';
import GenreDetailsView from './views/GenreDetailsView';
import GoogleView from './views/GoogleView';
import VerifiedView from './views/VerifiedView';
import SigninView from './views/SigninView';
import SignupView from './views/SignupView';
// Util function to hide route props in URL
const removeMe = () => '';
@@ -97,15 +98,13 @@ const publicRoutes = () =>
link: '/',
},
Login: {
component: (params: RouteProps<{}>) =>
AuthenticationView({ isSignup: false, ...params }),
options: { title: translate('signInBtn') },
component: SigninView,
options: { title: translate('signInBtn'), headerShown: false },
link: '/login',
},
Signup: {
component: (params: RouteProps<{}>) =>
AuthenticationView({ isSignup: true, ...params }),
options: { title: translate('signUpBtn') },
component: SignupView,
options: { title: translate('signUpBtn'), headerShown: false },
link: '/signup',
},
Oops: {

BIN
front/assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@@ -1,6 +1,5 @@
import { Box, useBreakpointValue, useTheme } from 'native-base';
import { LineChart } from 'react-native-chart-kit';
import { CardBorderRadius } from './Card';
import SongHistory from '../models/SongHistory';
import { useState } from 'react';
@@ -36,8 +35,7 @@ const ScoreGraph = (props: ScoreGraphProps) => {
return (
<Box
bgColor={theme.colors.primary[500]}
style={{ width: '100%', borderRadius: CardBorderRadius }}
style={{ width: '100%' }}
onLayout={(event) => setContainerWidth(event.nativeEvent.layout.width)}
>
<LineChart
@@ -48,10 +46,12 @@ const ScoreGraph = (props: ScoreGraphProps) => {
data: scores.map(({ score }) => score),
},
],
}}
}
}
width={containerWidth}
height={200} // Completely arbitrary
transparent={true}
withDots={false}
yAxisSuffix=" pts"
chartConfig={{
decimalPlaces: 0,
@@ -63,13 +63,13 @@ const ScoreGraph = (props: ScoreGraphProps) => {
},
}}
bezier
style={{
margin: 3,
shadowColor: theme.colors.primary[400],
shadowOpacity: 1,
shadowRadius: 20,
borderRadius: CardBorderRadius,
}}
// style={{
// margin: 3,
// shadowColor: theme.colors.primary[400],
// shadowOpacity: 1,
// shadowRadius: 20,
// borderRadius: CardBorderRadius,
// }}
/>
</Box>
);

View File

@@ -2,15 +2,16 @@ import React, { useState } from 'react';
import { StyleSheet, ActivityIndicator, View, Image, StyleProp, ViewStyle } from 'react-native';
import InteractiveBase from './InteractiveBase';
import { Text, useTheme } from 'native-base';
import { Icon } from 'iconsax-react-native';
interface ButtonProps {
title?: string;
style?: StyleProp<ViewStyle>;
onPress?: () => Promise<void>;
isDisabled?: boolean;
icon?: (size: string, color: string) => React.ReactNode;
icon?: Icon;
iconImage?: string;
type: 'filled' | 'outlined' | 'menu';
type?: 'filled' | 'outlined' | 'menu';
}
const ButtonBase: React.FC<ButtonProps> = ({
@@ -88,6 +89,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
});
const typeToStyleAnimator = { filled: styleButton, outlined: styleButton, menu: styleMenu };
const MyIcon: Icon = icon as Icon;
return (
<InteractiveBase
@@ -110,7 +112,7 @@ const ButtonBase: React.FC<ButtonProps> = ({
/>
) : (
<View style={styles.content}>
{icon && icon('18', type === 'outlined' ? '#6075F9' : '#FFFFFF')}
{icon && <MyIcon size={'18'} color={type === 'outlined' ? '#6075F9' : '#FFFFFF'}/>}
{iconImage && <Image source={{ uri: iconImage }} style={styles.icon} />}
{title && <Text style={styles.text}>{title}</Text>}
</View>
@@ -132,7 +134,6 @@ const styles = StyleSheet.create({
icon: {
width: 18,
height: 18,
// marginRight: 8,
},
text: {
color: '#fff',

View File

@@ -225,8 +225,18 @@ const InteractiveBase: React.FC<InteractiveBaseProps> = ({
elevation: elevationValue,
};
const disableStyle = {
backgroundColor: isOutlined ? 'rgba(0,0,0,0.3)' : styleAnimate.Disabled.backgroundColor,
borderColor: isOutlined ? styleAnimate.Disabled.backgroundColor : 'transparent',
borderWidth: 2,
scale: styleAnimate.Disabled.scale,
shadowOpacity: styleAnimate.Disabled.shadowOpacity,
shadowRadius: styleAnimate.Disabled.shadowRadius,
elevation: styleAnimate.Disabled.elevation,
}
return (
<Animated.View style={[style, isDisabled ? styleAnimate.Disabled : animatedStyle]}>
<Animated.View style={[style, isDisabled ? disableStyle : animatedStyle]}>
<Pressable
disabled={isDisabled}
onHoverIn={handleMouseEnter}

View File

@@ -0,0 +1,74 @@
import { LinearGradient } from "expo-linear-gradient";
import { Center, Flex, Stack, View, Text, Wrap, Image } 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 SeparatorBase from "./SeparatorBase";
import LinkBase from "./LinkBase";
import ImageBanner from '../../assets/banner.jpg';
interface ScaffoldAuthProps {
title: string;
description: string;
form: React.ReactNode[];
submitButton: React.ReactNode;
link: {text: string, description: string, onPress: () => void};
}
const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({title, description, form, submitButton, link}) => {
const layout = useWindowDimensions();
return (
<Flex direction='row' 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`)}
/>
<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>
</View>
</Center>
{
layout.width > 650 ?
<View style={{width: '50%', height: '100%', padding: 16}}>
<Image
source={ImageBanner}
alt="banner page"
style={{width: '100%', height: '100%', borderRadius: 8}}
/>
</View>
: <></>
}
<LinearGradient
start={{x: 0, y: 0}}
end={{x: 1, y: 1}}
colors={['#101014', '#6075F9']}
style={{top: 0, bottom: 0, right: 0, left: 0, width: '100%', height: '100%', position: 'absolute', zIndex: -2}}
/>
</Flex>
);
};
export default ScaffoldAuth;

View File

@@ -11,7 +11,7 @@ const styles = StyleSheet.create({
width: '100%',
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
marginVertical: 2,
},
text: {
color: 'white',

View File

@@ -1,4 +1,4 @@
import { Eye, EyeSlash } from 'iconsax-react-native';
import { Eye, EyeSlash, Icon } from 'iconsax-react-native';
import React, { useState } from 'react';
import { View, TouchableOpacity, StyleSheet, StyleProp, ViewStyle } from 'react-native';
import InteractiveBase from './InteractiveBase';
@@ -7,7 +7,7 @@ import { Input } from 'native-base';
export interface TextFieldBaseProps {
style?: StyleProp<ViewStyle>;
value?: string;
icon?: (size: string, color: string) => React.ReactNode;
icon?: Icon;
iconColor?: string;
placeholder?: string;
autoComplete?:
@@ -66,6 +66,7 @@ const TextFieldBase: React.FC<TextFieldBaseProps> = ({
}) => {
const [isPasswordVisible, setPasswordVisible] = useState(!isSecret);
const [isFocused, setFocused] = useState(false);
const MyIcon: Icon = icon as Icon;
const styleAnimate = StyleSheet.create({
Default: {
@@ -102,7 +103,7 @@ const TextFieldBase: React.FC<TextFieldBaseProps> = ({
<InteractiveBase style={[style, { borderRadius: 12 }]} styleAnimate={styleAnimate}>
<View style={styles.container}>
<View style={styles.iconContainerLeft}>
{icon && icon('20', iconColor ? iconColor : isFocused ? '#5f74f7' : '#394694')}
{icon && <MyIcon size={'20'} color={iconColor ? iconColor : isFocused ? '#5f74f7' : '#394694'} variant="Bold"/>}
</View>
<Input
variant="unstyled"

View File

@@ -59,9 +59,7 @@ const TextFormField: React.FC<TextFormFieldProps> = ({ error, style, ...textFiel
const styles = StyleSheet.create({
wrapper: {
flex: 1,
width: '100%',
// maxWidth: 400,
},
errorContainer: {
flexDirection: 'row',

View File

@@ -1,17 +0,0 @@
import React from 'react';
import { Provider } from 'react-redux';
import TestRenderer from 'react-test-renderer';
import store from '../state/Store';
import AuthenticationView from '../views/AuthenticationView';
describe('<AuthenticationView />', () => {
it('has 3 children', () => {
const tree = TestRenderer.create(
<Provider store={store}>
<AuthenticationView />
</Provider>
).toJSON();
expect(tree.children.length).toBe(3);
});
});

View File

@@ -1,103 +0,0 @@
import React from 'react';
import { useDispatch } from '../state/Store';
import { Translate, translate } from '../i18n/i18n';
import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { Center, Button, Text } from 'native-base';
import SigninForm from '../components/forms/signinform';
import SignupForm from '../components/forms/signupform';
import TextButton from '../components/TextButton';
import { RouteProps, useNavigation } from '../Navigation';
import * as Linking from 'expo-linking';
const hanldeSignin = async (
username: string,
password: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try {
const apiAccess = await API.authenticate({ username, password });
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate('unknownError');
}
};
const handleSignup = async (
username: string,
password: string,
email: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try {
const apiAccess = await API.createAccount({ username, password, email });
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) {
if (error.status === 409) return translate('usernameTaken');
return translate(error.userMessage);
}
if (error instanceof Error) return error.message;
return translate('unknownError');
}
};
type AuthenticationViewProps = {
isSignup: boolean;
};
const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) => {
const dispatch = useDispatch();
const navigation = useNavigation();
const mode = isSignup ? 'signup' : 'signin';
return (
<Center style={{ flex: 1 }}>
<Text>
<Translate translationKey="welcome" />
</Text>
<TextButton
translate={{ translationKey: 'continuewithgoogle' }}
variant="outline"
marginTop={5}
colorScheme="primary"
onPress={() => Linking.openURL(`${API.baseUrl}/auth/login/google`)}
/>
{mode === 'signin' ? (
<SigninForm
onSubmit={(username, password) =>
hanldeSignin(username, password, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
/>
) : (
<SignupForm
onSubmit={(username, password, email) =>
handleSignup(username, password, email, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
/>
)}
{mode === 'signin' && (
<Button variant="outline" marginTop={5} colorScheme="error">
{translate('forgottenPassword')}
</Button>
)}
<TextButton
translate={{ translationKey: mode === 'signin' ? 'signUpBtn' : 'signInBtn' }}
variant="outline"
marginTop={5}
colorScheme="primary"
onPress={() => navigation.navigate(mode === 'signin' ? 'Signup' : 'Login', {})}
/>
</Center>
);
};
export default AuthenticationView;

View File

@@ -1,14 +1,131 @@
import React from 'react';
import { View } from 'react-native';
import { Box, Heading, HStack } from 'native-base';
import { useWindowDimensions, View } from 'react-native';
import { Box, Column, Flex, Heading, HStack, Progress, Row, Text, VStack, Wrap } from 'native-base';
import { useNavigation } from '../Navigation';
import TextButton from '../components/TextButton';
import UserAvatar from '../components/UserAvatar';
import { LoadingView } from '../components/Loading';
import { useQuery } from '../Queries';
import API from '../API';
import { LinearGradient } from 'expo-linear-gradient';
import ButtonBase from '../components/UI/ButtonBase';
import { translate } from '../i18n/i18n';
import ScoreGraph from '../components/ScoreGraph';
const fakeData = [
{score: 47, songID: 34, userID: 1, playDate: new Date("2023-08-20 8:27:21"), difficulties: 1},
{score: 1, songID: 603, userID: 1, playDate: new Date("2023-09-13 22:56:45"), difficulties: 1},
{score: 93, songID: 601, userID: 1, playDate: new Date("2023-08-11 5:30:13"), difficulties: 5},
{score: 55, songID: 456, userID: 1, playDate: new Date("2023-07-10 23:06:09"), difficulties: 4},
{score: 2, songID: 345, userID: 1, playDate: new Date("2023-07-23 18:33:24"), difficulties: 2},
{score: 47, songID: 625, userID: 1, playDate: new Date("2023-07-09 7:16:46"), difficulties: 1},
{score: 27, songID: 234, userID: 1, playDate: new Date("2023-07-06 15:56:53"), difficulties: 5},
{score: 85, songID: 866, userID: 1, playDate: new Date("2023-07-08 8:56:44"), difficulties: 2},
{score: 28, songID: 484, userID: 1, playDate: new Date("2023-09-12 6:05:32"), difficulties: 4},
{score: 5, songID: 443, userID: 1, playDate: new Date("2023-08-01 11:57:09"), difficulties: 3},
{score: 14, songID: 109, userID: 1, playDate: new Date("2023-07-03 22:54:07"), difficulties: 3},
{score: 57, songID: 892, userID: 1, playDate: new Date("2023-07-13 23:22:34"), difficulties: 5},
{score: 7, songID: 164, userID: 1, playDate: new Date("2023-07-02 0:15:13"), difficulties: 2},
{score: 42, songID: 761, userID: 1, playDate: new Date("2023-07-10 18:25:19"), difficulties: 3},
{score: 49, songID: 82, userID: 1, playDate: new Date("2023-09-12 12:51:15"), difficulties: 4},
{score: 83, songID: 488, userID: 1, playDate: new Date("2023-08-28 7:56:31"), difficulties: 5},
{score: 91, songID: 648, userID: 1, playDate: new Date("2023-07-21 10:16:33"), difficulties: 4},
{score: 67, songID: 210, userID: 1, playDate: new Date("2023-09-14 8:04:50"), difficulties: 1},
{score: 31, songID: 274, userID: 1, playDate: new Date("2023-07-10 11:24:28"), difficulties: 4},
{score: 29, songID: 930, userID: 1, playDate: new Date("2023-08-06 0:05:43"), difficulties: 5},
{score: 51, songID: 496, userID: 1, playDate: new Date("2023-08-14 9:43:14"), difficulties: 1},
{score: 56, songID: 370, userID: 1, playDate: new Date("2023-08-18 19:25:59"), difficulties: 2},
{score: 29, songID: 333, userID: 1, playDate: new Date("2023-07-11 4:26:44"), difficulties: 4},
{score: 95, songID: 921, userID: 1, playDate: new Date("2023-08-30 12:58:50"), difficulties: 1},
{score: 37, songID: 80, userID: 1, playDate: new Date("2023-07-16 7:17:57"), difficulties: 4},
{score: 90, songID: 134, userID: 1, playDate: new Date("2023-09-03 9:00:04"), difficulties: 1},
{score: 51, songID: 497, userID: 1, playDate: new Date("2023-07-31 19:34:43"), difficulties: 4},
{score: 95, songID: 368, userID: 1, playDate: new Date("2023-09-12 20:12:50"), difficulties: 4},
{score: 55, songID: 247, userID: 1, playDate: new Date("2023-09-16 2:45:13"), difficulties: 1},
{score: 26, songID: 725, userID: 1, playDate: new Date("2023-07-28 22:59:31"), difficulties: 2},
{score: 82, songID: 952, userID: 1, playDate: new Date("2023-08-01 6:31:47"), difficulties: 1},
{score: 88, songID: 85, userID: 1, playDate: new Date("2023-08-12 2:33:11"), difficulties: 5},
{score: 12, songID: 96, userID: 1, playDate: new Date("2023-09-03 14:00:33"), difficulties: 4},
{score: 100, songID: 807, userID: 1, playDate: new Date("2023-07-03 0:53:11"), difficulties: 3},
{score: 88, songID: 456, userID: 1, playDate: new Date("2023-08-06 9:17:15"), difficulties: 5},
{score: 10, songID: 889, userID: 1, playDate: new Date("2023-08-15 12:19:16"), difficulties: 3},
{score: 76, songID: 144, userID: 1, playDate: new Date("2023-09-10 2:56:49"), difficulties: 4},
{score: 60, songID: 808, userID: 1, playDate: new Date("2023-07-24 10:22:33"), difficulties: 1},
{score: 94, songID: 537, userID: 1, playDate: new Date("2023-08-03 23:22:29"), difficulties: 2},
{score: 100, songID: 465, userID: 1, playDate: new Date("2023-09-16 19:12:58"), difficulties: 2},
{score: 85, songID: 31, userID: 1, playDate: new Date("2023-08-17 5:29:49"), difficulties: 2},
{score: 98, songID: 345, userID: 1, playDate: new Date("2023-09-11 1:51:49"), difficulties: 1},
{score: 81, songID: 204, userID: 1, playDate: new Date("2023-08-21 2:46:56"), difficulties: 2},
{score: 21, songID: 40, userID: 1, playDate: new Date("2023-07-27 4:00:00"), difficulties: 2},
{score: 91, songID: 274, userID: 1, playDate: new Date("2023-07-14 16:09:49"), difficulties: 5},
{score: 99, songID: 416, userID: 1, playDate: new Date("2023-08-27 1:56:16"), difficulties: 5},
{score: 58, songID: 87, userID: 1, playDate: new Date("2023-09-08 19:30:20"), difficulties: 5},
{score: 90, songID: 744, userID: 1, playDate: new Date("2023-08-18 23:47:55"), difficulties: 2},
{score: 69, songID: 954, userID: 1, playDate: new Date("2023-08-07 1:55:52"), difficulties: 5},
{score: 75, songID: 467, userID: 1, playDate: new Date("2023-07-10 8:37:22"), difficulties: 4},
{score: 41, songID: 693, userID: 1, playDate: new Date("2023-09-11 5:15:16"), difficulties: 2},
{score: 56, songID: 140, userID: 1, playDate: new Date("2023-08-06 5:32:46"), difficulties: 2},
{score: 88, songID: 64, userID: 1, playDate: new Date("2023-07-31 20:24:30"), difficulties: 1},
{score: 99, songID: 284, userID: 1, playDate: new Date("2023-08-07 17:51:19"), difficulties: 5},
{score: 47, songID: 746, userID: 1, playDate: new Date("2023-07-18 17:45:56"), difficulties: 5},
{score: 80, songID: 791, userID: 1, playDate: new Date("2023-08-21 1:19:45"), difficulties: 1},
{score: 21, songID: 748, userID: 1, playDate: new Date("2023-07-04 9:09:27"), difficulties: 4},
{score: 75, songID: 541, userID: 1, playDate: new Date("2023-09-19 23:08:05"), difficulties: 2},
{score: 31, songID: 724, userID: 1, playDate: new Date("2023-07-09 2:01:29"), difficulties: 4},
{score: 24, songID: 654, userID: 1, playDate: new Date("2023-09-04 1:27:00"), difficulties: 1},
{score: 55, songID: 154, userID: 1, playDate: new Date("2023-07-10 17:48:17"), difficulties: 3},
{score: 4, songID: 645, userID: 1, playDate: new Date("2023-09-11 18:51:11"), difficulties: 2},
{score: 52, songID: 457, userID: 1, playDate: new Date("2023-07-30 19:12:52"), difficulties: 3},
{score: 68, songID: 236, userID: 1, playDate: new Date("2023-08-08 8:56:08"), difficulties: 3},
{score: 44, songID: 16, userID: 1, playDate: new Date("2023-07-22 10:39:34"), difficulties: 1},
{score: 59, songID: 863, userID: 1, playDate: new Date("2023-09-17 4:12:43"), difficulties: 1},
{score: 18, songID: 276, userID: 1, playDate: new Date("2023-07-08 15:47:54"), difficulties: 2},
{score: 64, songID: 557, userID: 1, playDate: new Date("2023-08-17 0:13:46"), difficulties: 1},
{score: 2, songID: 452, userID: 1, playDate: new Date("2023-07-26 5:13:31"), difficulties: 5},
{score: 99, songID: 546, userID: 1, playDate: new Date("2023-07-11 16:31:37"), difficulties: 1},
{score: 75, songID: 598, userID: 1, playDate: new Date("2023-08-12 22:56:24"), difficulties: 4},
{score: 4, songID: 258, userID: 1, playDate: new Date("2023-09-20 8:26:50"), difficulties: 2},
{score: 50, songID: 190, userID: 1, playDate: new Date("2023-09-20 20:07:06"), difficulties: 4},
{score: 9, songID: 914, userID: 1, playDate: new Date("2023-08-30 16:57:14"), difficulties: 5},
{score: 7, songID: 92, userID: 1, playDate: new Date("2023-07-18 20:33:44"), difficulties: 5},
{score: 94, songID: 98, userID: 1, playDate: new Date("2023-08-15 5:05:18"), difficulties: 5},
{score: 94, songID: 424, userID: 1, playDate: new Date("2023-07-22 9:59:12"), difficulties: 5},
{score: 14, songID: 635, userID: 1, playDate: new Date("2023-07-02 6:58:39"), difficulties: 4},
{score: 99, songID: 893, userID: 1, playDate: new Date("2023-08-05 16:09:33"), difficulties: 1},
{score: 94, songID: 67, userID: 1, playDate: new Date("2023-07-01 8:11:37"), difficulties: 2},
{score: 21, songID: 335, userID: 1, playDate: new Date("2023-08-03 2:07:44"), difficulties: 3},
{score: 47, songID: 294, userID: 1, playDate: new Date("2023-09-13 17:32:46"), difficulties: 4},
{score: 89, songID: 184, userID: 1, playDate: new Date("2023-07-04 5:20:13"), difficulties: 2},
{score: 28, songID: 345, userID: 1, playDate: new Date("2023-09-07 6:35:11"), difficulties: 3},
{score: 93, songID: 697, userID: 1, playDate: new Date("2023-07-29 0:07:10"), difficulties: 2},
{score: 58, songID: 666, userID: 1, playDate: new Date("2023-07-09 3:03:02"), difficulties: 2},
{score: 73, songID: 459, userID: 1, playDate: new Date("2023-08-05 7:33:54"), difficulties: 4},
{score: 50, songID: 695, userID: 1, playDate: new Date("2023-07-26 18:26:55"), difficulties: 4},
{score: 39, songID: 995, userID: 1, playDate: new Date("2023-08-24 17:34:09"), difficulties: 3},
{score: 25, songID: 122, userID: 1, playDate: new Date("2023-08-25 18:54:12"), difficulties: 1},
{score: 29, songID: 439, userID: 1, playDate: new Date("2023-09-15 0:44:48"), difficulties: 3},
{score: 79, songID: 234, userID: 1, playDate: new Date("2023-09-13 13:53:16"), difficulties: 2},
{score: 0, songID: 369, userID: 1, playDate: new Date("2023-08-30 22:54:34"), difficulties: 1},
{score: 25, songID: 223, userID: 1, playDate: new Date("2023-09-13 1:09:11"), difficulties: 3},
{score: 55, songID: 716, userID: 1, playDate: new Date("2023-07-12 19:43:23"), difficulties: 3},
{score: 100, songID: 62, userID: 1, playDate: new Date("2023-07-11 15:33:40"), difficulties: 5},
{score: 74, songID: 271, userID: 1, playDate: new Date("2023-08-25 23:14:51"), difficulties: 3},
{score: 22, songID: 265, userID: 1, playDate: new Date("2023-07-17 15:01:38"), difficulties: 1},
{score: 79, songID: 552, userID: 1, playDate: new Date("2023-07-28 20:13:14"), difficulties: 5},
{score: 50, songID: 603, userID: 1, playDate: new Date("2023-07-06 3:52:21"), difficulties: 5},
];
function xpToLevel(xp: number): number {
return Math.floor(xp / 1000);
}
function xpToProgressBarValue(xp: number): number {
return Math.floor(xp / 10);
}
const ProfileView = () => {
const layout = useWindowDimensions();
const navigation = useNavigation();
const userQuery = useQuery(API.getUserInfo);
@@ -16,22 +133,59 @@ const ProfileView = () => {
return <LoadingView />;
}
// const user = userQuery.data;
const progessValue = xpToProgressBarValue(userQuery.data.data.xp);
const level = xpToLevel(userQuery.data.data.xp);
return (
<View style={{ flexDirection: 'column' }}>
<HStack space={3} marginY={10} marginX={10}>
<UserAvatar size="lg" />
<Box>
<Heading>{userQuery.data.name}</Heading>
<Heading>XP : {userQuery.data.data.xp}</Heading>
</Box>
</HStack>
<Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50}>
<TextButton
onPress={() => navigation.navigate('Settings')}
translate={{ translationKey: 'settingsBtn' }}
/>
</Box>
</View>
<Flex style={{ minHeight: layout.height, height: '100%', padding: 20}} >
<Wrap style={{flexDirection: layout.width > 650 ? 'row' : 'column', alignItems: 'center', paddingBottom: 20, justifyContent: 'space-between'}}>
<UserAvatar size={"2xl"}/>
<Column style={{paddingLeft: layout.width > 650 ? 20 : 0, paddingTop: layout.width > 650 ? 0 : 20, flex: 1, width: '100%'}}>
<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: 20 }}>Dernier entraînement il y a une semaine</Text>
<Wrap style={{flexDirection: 'row', alignItems: 'center', paddingBottom: 20}}>
<Text style={{paddingRight: 20}}>32 Completes</Text>
<Text>42 En cours</Text>
</Wrap>
</Column>
</Wrap>
<Row style={{alignItems: 'center', paddingBottom: 40}}>
<Text style={{paddingRight: 20}}>{`${translate('level')} ${level}`}</Text>
<Progress value={progessValue} w={'2/3'} maxW={'400'}/>
</Row>
<ScoreGraph songHistory={
{
history: fakeData,
best: 200
}
} />
<LinearGradient
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
colors={['#101014', '#6075F9']}
style={{
top: 0,
bottom: 0,
right: 0,
left: 0,
width: '100%',
height: '100%',
margin: 0,
padding: 0,
position: 'absolute',
zIndex: -2,
}}
/>
</Flex>
);
};

141
front/views/SigninView.tsx Normal file
View File

@@ -0,0 +1,141 @@
import React from 'react';
import { useDispatch } from '../state/Store';
import { translate } from '../i18n/i18n';
import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { string } from 'yup';
import { useToast } from 'native-base';
import TextFormField from '../components/UI/TextFormField';
import ButtonBase from '../components/UI/ButtonBase';
import { Lock1, User } from 'iconsax-react-native';
import ScaffoldAuth from '../components/UI/ScaffoldAuth';
import { useNavigation } from '../Navigation';
import LinkBase from '../components/UI/LinkBase';
const hanldeSignin = async (
username: string,
password: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try {
const apiAccess = await API.authenticate({ username, password });
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate('unknownError');
}
};
const SigninView = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const [formData, setFormData] = React.useState({
username: {
value: '',
error: null as string | null,
},
password: {
value: '',
error: null as string | null,
},
});
const validationSchemas = {
username: string()
.min(3, translate('usernameTooShort'))
.max(20, translate('usernameTooLong'))
.required('Username is required'),
password: string()
.min(4, translate('passwordTooShort'))
.max(100, translate('passwordTooLong'))
.required('Password is required'),
};
const toast = useToast();
const onSubmit= (username: string, password: string) => {
return hanldeSignin(username, password, (accessToken) =>
dispatch(setAccessToken(accessToken))
);
}
return (
<ScaffoldAuth
title="Bienvenue !"
description="Continuez avec Google ou entrez vos coordonnées."
form={[
<TextFormField
error={formData.username.error}
icon={User}
placeholder="Username"
autoComplete="username"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
isRequired
/>,
<TextFormField
error={formData.password.error}
icon={Lock1}
placeholder="Password"
autoComplete="password"
value={formData.password.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, password: { value: t, error } });
});
}}
isRequired
/>,
<LinkBase onPress={() => console.log('Link clicked!')}>
{translate('forgottenPassword')}
</LinkBase>,
]}
submitButton={
<ButtonBase
style={{width: '100%'}}
title="Signin"
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
formData.username.value === '' ||
formData.password.value === ''
}
onPress={async () => {
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value
);
toast.show({ description: resp, colorScheme: 'secondary' });
} catch (e) {
toast.show({
description: e as string,
colorScheme: 'red',
avoidKeyboard: true,
});
}
}}
/>
}
link={{
text: "Inscrivez-vous gratuitement",
description: "Vous n'avez pas de compte ? ",
onPress: () => navigation.navigate('Signup')
}}
/>
);
};
export default SigninView;

200
front/views/SignupView.tsx Normal file
View File

@@ -0,0 +1,200 @@
import React from 'react';
import { useDispatch } from '../state/Store';
import { translate } from '../i18n/i18n';
import API, { APIError } from '../API';
import { setAccessToken } from '../state/UserSlice';
import { string } from 'yup';
import { useToast } from 'native-base';
import TextFormField from '../components/UI/TextFormField';
import ButtonBase from '../components/UI/ButtonBase';
import { Lock1, Sms, User } from 'iconsax-react-native';
import ScaffoldAuth from '../components/UI/ScaffoldAuth';
import { useNavigation } from '../Navigation';
const handleSignup = async (
username: string,
password: string,
email: string,
apiSetter: (accessToken: string) => void
): Promise<string> => {
try {
const apiAccess = await API.createAccount({ username, password, email });
apiSetter(apiAccess);
return translate('loggedIn');
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate('unknownError');
}
};
const SignupView = () => {
const dispatch = useDispatch();
const navigation = useNavigation();
const [formData, setFormData] = React.useState({
username: {
value: '',
error: null as string | null,
},
password: {
value: '',
error: null as string | null,
},
repeatPassword: {
value: '',
error: null as string | null,
},
email: {
value: '',
error: null as string | null,
},
});
const validationSchemas = {
username: string()
.min(3, translate('usernameTooShort'))
.max(20, translate('usernameTooLong'))
.required('Username is required'),
email: string().email('Invalid email').required('Email is required'),
password: string()
.min(4, translate('passwordTooShort'))
.max(100, translate('passwordTooLong'))
// .matches(
// /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$-_%\^&\*])(?=.{8,})/,
// translate(
// "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character"
// )
// )
.required('Password is required'),
};
const toast = useToast();
const onSubmit= (username: string, email: string, password: string) => {
return handleSignup(username, password, email, (accessToken) =>
dispatch(setAccessToken(accessToken))
)
}
return (
<ScaffoldAuth
title="Créer un compte"
description="Apprendre le piano gratuitement et de manière ludique"
form={[
<TextFormField
error={formData.username.error}
icon={User}
placeholder="Username"
autoComplete="username"
value={formData.username.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.username
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
isRequired
/>,
<TextFormField
error={formData.email.error}
icon={Sms}
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 } });
});
}}
isRequired
/>,
<TextFormField
isRequired
isSecret
error={formData.password.error}
icon={Lock1}
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');
}
setFormData({
...formData,
repeatPassword: { value: t, error },
});
});
}}
/>
]}
submitButton={
<ButtonBase
style={{width: '100%'}}
title="Signin"
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 === ''
}
onPress={async () => {
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value,
formData.email.value
);
toast.show({ description: resp, colorScheme: 'secondary' });
} catch (e) {
toast.show({
description: e as string,
colorScheme: 'red',
avoidKeyboard: true,
});
}
}}
/>
}
link={{
text: "S'identifier",
description: "Vous avez déjà un compte ? ",
onPress: () => navigation.navigate('Login')
}}
/>
);
};
export default SignupView;