diff --git a/front/Navigation.tsx b/front/Navigation.tsx index 486ac99..6624786 100644 --- a/front/Navigation.tsx +++ b/front/Navigation.tsx @@ -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: { diff --git a/front/assets/banner.jpg b/front/assets/banner.jpg new file mode 100644 index 0000000..2767e1c Binary files /dev/null and b/front/assets/banner.jpg differ diff --git a/front/components/ScoreGraph.tsx b/front/components/ScoreGraph.tsx index 01f3565..741522c 100644 --- a/front/components/ScoreGraph.tsx +++ b/front/components/ScoreGraph.tsx @@ -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 ( setContainerWidth(event.nativeEvent.layout.width)} > { 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, + // }} /> ); diff --git a/front/components/UI/ButtonBase.tsx b/front/components/UI/ButtonBase.tsx index d67757e..0834d71 100644 --- a/front/components/UI/ButtonBase.tsx +++ b/front/components/UI/ButtonBase.tsx @@ -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; onPress?: () => Promise; 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 = ({ @@ -88,6 +89,7 @@ const ButtonBase: React.FC = ({ }); const typeToStyleAnimator = { filled: styleButton, outlined: styleButton, menu: styleMenu }; + const MyIcon: Icon = icon as Icon; return ( = ({ /> ) : ( - {icon && icon('18', type === 'outlined' ? '#6075F9' : '#FFFFFF')} + {icon && } {iconImage && } {title && {title}} @@ -132,7 +134,6 @@ const styles = StyleSheet.create({ icon: { width: 18, height: 18, - // marginRight: 8, }, text: { color: '#fff', diff --git a/front/components/UI/InteractiveBase.tsx b/front/components/UI/InteractiveBase.tsx index 8022cb2..502f6c3 100644 --- a/front/components/UI/InteractiveBase.tsx +++ b/front/components/UI/InteractiveBase.tsx @@ -225,8 +225,18 @@ const InteractiveBase: React.FC = ({ 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 ( - + void}; +} + +const ScaffoldAuth: FunctionComponent = ({title, description, form, submitButton, link}) => { + const layout = useWindowDimensions(); + + return ( + +
+ + + {title} + {description} + + + Linking.openURL(`${API.baseUrl}/auth/login/google`)} + /> + or + + {form} + + {submitButton} + + {link.description} + + {link.text} + + + + +
+ { + layout.width > 650 ? + + banner page + + : <> + } + +
+ ); +}; + +export default ScaffoldAuth; diff --git a/front/components/UI/SeparatorBase.tsx b/front/components/UI/SeparatorBase.tsx index 2ce23d4..2f94c79 100644 --- a/front/components/UI/SeparatorBase.tsx +++ b/front/components/UI/SeparatorBase.tsx @@ -11,7 +11,7 @@ const styles = StyleSheet.create({ width: '100%', flexDirection: 'row', alignItems: 'center', - marginVertical: 20, + marginVertical: 2, }, text: { color: 'white', diff --git a/front/components/UI/TextFieldBase.tsx b/front/components/UI/TextFieldBase.tsx index 345c2b2..818f603 100644 --- a/front/components/UI/TextFieldBase.tsx +++ b/front/components/UI/TextFieldBase.tsx @@ -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; value?: string; - icon?: (size: string, color: string) => React.ReactNode; + icon?: Icon; iconColor?: string; placeholder?: string; autoComplete?: @@ -66,6 +66,7 @@ const TextFieldBase: React.FC = ({ }) => { 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 = ({ - {icon && icon('20', iconColor ? iconColor : isFocused ? '#5f74f7' : '#394694')} + {icon && } = ({ error, style, ...textFiel const styles = StyleSheet.create({ wrapper: { - flex: 1, width: '100%', - // maxWidth: 400, }, errorContainer: { flexDirection: 'row', diff --git a/front/views/AuthenticationView.test.tsx b/front/views/AuthenticationView.test.tsx deleted file mode 100644 index 6814e23..0000000 --- a/front/views/AuthenticationView.test.tsx +++ /dev/null @@ -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('', () => { - it('has 3 children', () => { - const tree = TestRenderer.create( - - - - ).toJSON(); - expect(tree.children.length).toBe(3); - }); -}); diff --git a/front/views/AuthenticationView.tsx b/front/views/AuthenticationView.tsx deleted file mode 100644 index 36ff7b9..0000000 --- a/front/views/AuthenticationView.tsx +++ /dev/null @@ -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 => { - 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 => { - 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) => { - const dispatch = useDispatch(); - const navigation = useNavigation(); - const mode = isSignup ? 'signup' : 'signin'; - - return ( -
- - - - Linking.openURL(`${API.baseUrl}/auth/login/google`)} - /> - {mode === 'signin' ? ( - - hanldeSignin(username, password, (accessToken) => - dispatch(setAccessToken(accessToken)) - ) - } - /> - ) : ( - - handleSignup(username, password, email, (accessToken) => - dispatch(setAccessToken(accessToken)) - ) - } - /> - )} - {mode === 'signin' && ( - - )} - navigation.navigate(mode === 'signin' ? 'Signup' : 'Login', {})} - /> -
- ); -}; - -export default AuthenticationView; diff --git a/front/views/ProfileView.tsx b/front/views/ProfileView.tsx index 004c5c1..d66b7f0 100644 --- a/front/views/ProfileView.tsx +++ b/front/views/ProfileView.tsx @@ -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 ; } + // const user = userQuery.data; + const progessValue = xpToProgressBarValue(userQuery.data.data.xp); + const level = xpToLevel(userQuery.data.data.xp); + return ( - - - - - {userQuery.data.name} - XP : {userQuery.data.data.xp} - - - - navigation.navigate('Settings')} - translate={{ translationKey: 'settingsBtn' }} - /> - - + + 650 ? 'row' : 'column', alignItems: 'center', paddingBottom: 20, justifyContent: 'space-between'}}> + + 650 ? 20 : 0, paddingTop: layout.width > 650 ? 0 : 20, flex: 1, width: '100%'}}> + + {userQuery.data.name} + navigation.navigate('Settings')} + /> + + Dernier entraînement il y a une semaine + + 32 Completes + 42 En cours + + + + + {`${translate('level')} ${level}`} + + + + + ); }; diff --git a/front/views/SigninView.tsx b/front/views/SigninView.tsx new file mode 100644 index 0000000..c82973a --- /dev/null +++ b/front/views/SigninView.tsx @@ -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 => { + 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 ( + { + let error: null | string = null; + validationSchemas.username + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, username: { value: t, error } }); + }); + }} + isRequired + />, + { + let error: null | string = null; + validationSchemas.password + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, password: { value: t, error } }); + }); + }} + isRequired + />, + console.log('Link clicked!')}> + {translate('forgottenPassword')} + , + ]} + submitButton={ + { + 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; \ No newline at end of file diff --git a/front/views/SignupView.tsx b/front/views/SignupView.tsx new file mode 100644 index 0000000..51f4231 --- /dev/null +++ b/front/views/SignupView.tsx @@ -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 => { + 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 ( + { + let error: null | string = null; + validationSchemas.username + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, username: { value: t, error } }); + }); + }} + isRequired + />, + { + let error: null | string = null; + validationSchemas.email + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, email: { value: t, error } }); + }); + }} + isRequired + />, + { + let error: null | string = null; + validationSchemas.password + .validate(t) + .catch((e) => (error = e.message)) + .finally(() => { + setFormData({ ...formData, password: { value: t, error } }); + }); + }} + />, + { + 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={ + { + 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; \ No newline at end of file