redesign: signin & signup for the follow up

This commit is contained in:
mathysPaul
2023-07-30 02:37:07 +09:00
parent 509cc5b9f8
commit 4de28337a3
17 changed files with 1017 additions and 36 deletions

View File

@@ -65,7 +65,7 @@ export default class API {
public static readonly baseUrl =
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
? '/api'
: Constants.manifest?.extra?.apiUrl;
: "https://nightly.chroma.octohub.app/api";
public static async fetch(
params: FetchParams,
handle: Pick<Required<HandleParams>, 'raw'>

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Provider } from 'react-redux';
import { useEffect } from 'react';
import store, { persistor } from './state/Store';
import { Router } from './Navigation';
import './i18n/i18n';
@@ -10,12 +11,25 @@ import LanguageGate from './i18n/LanguageGate';
import ThemeProvider, { ColorSchemeProvider } from './Theme';
import 'react-native-url-polyfill/auto';
import { QueryRules } from './Queries';
import { useFonts } from 'expo-font';
const queryClient = new QueryClient(QueryRules);
export default function App() {
// SplashScreen.preventAutoHideAsync();
// setTimeout(SplashScreen.hideAsync, 500);
SplashScreen.preventAutoHideAsync();
setTimeout(SplashScreen.hideAsync, 500);
const [fontsLoaded] = useFonts({
'Lexend': require('./assets/fonts/Lexend-VariableFont_wght.ttf'),
});
useEffect(() => {
if (fontsLoaded) {
SplashScreen.hideAsync();
}
}, [fontsLoaded]);
return (
<Provider store={store}>

View File

@@ -12,6 +12,8 @@ import { useDispatch } from 'react-redux';
import { Translate, translate } from './i18n/i18n';
import SongLobbyView from './views/SongLobbyView';
import AuthenticationView from './views/AuthenticationView';
import SigninView from './views/SigninView';
import SignupView from './views/SignupView';
import StartPageView from './views/StartPageView';
import HomeView from './views/HomeView';
import SearchView from './views/SearchView';
@@ -84,17 +86,27 @@ 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',
},
// Login: {
// component: (params: RouteProps<{}>) =>
// AuthenticationView({ isSignup: false, ...params }),
// options: { title: translate('signInBtn') },
// link: '/login',
// },
// Signup: {
// component: (params: RouteProps<{}>) =>
// AuthenticationView({ isSignup: true, ...params }),
// options: { title: translate('signUpBtn') },
// link: '/signup',
// },
Oops: {
component: ProfileErrorView,
options: { title: 'Oops', headerShown: false },

View File

@@ -12,6 +12,11 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
useSystemColorMode: false,
initialColorMode: colorScheme,
},
fonts: {
heading: "Lexend",
body: "Lexend",
mono: "Lexend",
},
colors: {
primary: {
50: '#e6faea',

BIN
front/assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

View File

@@ -0,0 +1,147 @@
import React, { useRef, useState } from 'react';
import { Animated, StyleSheet, TouchableOpacity, Text, ActivityIndicator, View, Image } from 'react-native';
import Ionicons from '@expo/vector-icons/Ionicons';
interface ButtonProps {
title?: string;
onPress?: () => Promise<any>;
isDisabled?: boolean;
icon?: string;
iconImage?: string;
isCollapsed?: boolean;
isOutlined?: boolean;
}
const ButtonBase: React.FC<ButtonProps> = ({ title, onPress, isDisabled, icon, iconImage, isCollapsed, isOutlined = false }) => {
const shouldCollapse = isCollapsed !== undefined ? isCollapsed : (!title && (icon || iconImage));
const [loading, setLoading] = useState(false);
const scaleValue = useRef(new Animated.Value(1)).current;
const colorValue = useRef(new Animated.Value(0)).current;
const backgroundColor = colorValue.interpolate({
inputRange: [0, 1],
outputRange: isOutlined ? ['transparent', 'transparent'] : ['#6075F9', '#4352ae'],
});
const borderColor = colorValue.interpolate({
inputRange: [0, 1],
outputRange: ['#6075F9', '#4352ae'],
});
const textColor = colorValue.interpolate({
inputRange: [0, 1],
outputRange: isOutlined ? ['#6075F9', '#4352ae'] : ['#ffffff', '#ffffff'],
});
// scale animation onClick
const handlePressIn = () => {
Animated.spring(scaleValue, {
toValue: 0.98,
useNativeDriver: true,
}).start();
};
// scale animation reset after onClick
const handlePressOut = async () => {
Animated.spring(scaleValue, {
toValue: 1,
friction: 30,
// tension: 50,
useNativeDriver: true,
}).start();
if (onPress && !isDisabled) {
setLoading(true);
await onPress();
setLoading(false);
}
};
// color animation onHover
const handleMouseEnter = () => {
Animated.timing(colorValue, {
toValue: 1,
duration: 250,
useNativeDriver: false,
}).start();
};
// color animation reset after onHover
const handleMouseLeave = () => {
Animated.timing(colorValue, {
toValue: 0,
duration: 250,
useNativeDriver: false,
}).start();
};
return (
<View style={shouldCollapse ? styles.collapsedContainer : styles.container}>
<TouchableOpacity
activeOpacity={1}
onPressIn={handlePressIn}
onPressOut={handlePressOut}
disabled={isDisabled || loading}
style={styles.buttonContainer}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Animated.View
style={[
styles.button,
{
backgroundColor: isDisabled ? '#454562' : backgroundColor,
transform: [{ scale: scaleValue }],
borderWidth: isOutlined ? 2 : 0,
borderColor,
padding: (icon || iconImage) ? 12 : 16
},
]}
>
{loading ? (
<ActivityIndicator size="small" color={isOutlined ? '#6075F9' : '#FFFFFF'} />
) : (
<View style={styles.content}>
{icon && <Ionicons name={icon} size={24} color={isOutlined ? '#6075F9' : '#FFFFFF'} />}
{iconImage && <Image source={{uri: iconImage}} style={styles.icon} />}
{title && <Animated.Text style={[styles.text, { color: textColor }]}>{title}</Animated.Text>}
</View>
)}
</Animated.View>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
},
collapsedContainer: {
width: 'fit-content',
// alignItems: 'center',
},
buttonContainer: {
borderRadius: 16,
},
button: {
borderRadius: 16,
padding: 16,
justifyContent: 'center',
alignItems: 'center',
},
text: {
color: '#fff',
marginHorizontal: 8
},
content: {
flexDirection: 'row',
alignItems: 'center',
},
icon: {
width: 24,
height: 24,
// marginRight: 8,
},
});
export default ButtonBase;

View File

@@ -0,0 +1,23 @@
import React, { ReactNode, FunctionComponent } from 'react';
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
linkText: {
textDecorationLine: 'underline',
color: '#A3AFFC',
fontWeight: '700',
},
});
interface LinkBaseProps {
children: ReactNode;
onPress: () => void;
}
const LinkBase: FunctionComponent<LinkBaseProps> = ({ children, onPress }) => (
<TouchableOpacity onPress={onPress}>
<Text style={styles.linkText}>{children}</Text>
</TouchableOpacity>
);
export default LinkBase;

View File

@@ -0,0 +1,34 @@
import React, { FunctionComponent, ReactNode } from 'react';
import { View, Text, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
line: {
flex: 1,
height: 2,
backgroundColor: 'white',
},
container: {
width: '100%',
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
},
text: {
color: 'white',
paddingHorizontal: 16,
},
});
interface SeparatorBaseProps {
children: ReactNode;
}
const SeparatorBase: FunctionComponent<SeparatorBaseProps> = ({children}) => (
<View style={styles.container}>
<View style={styles.line} />
<Text style={styles.text}>{children}</Text>
<View style={styles.line} />
</View>
);
export default SeparatorBase;

View File

@@ -0,0 +1,109 @@
import React, { useState, useEffect } from 'react';
import { View, TextInput, TouchableOpacity, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; // Supposons que nous utilisons la bibliothèque Ionicons pour les icônes
export interface TextFieldBaseProps {
value?: string;
icon?: string;
iconColor?: string;
placeholder?: string;
autoComplete?:
| 'birthdate-day'
| 'birthdate-full'
| 'birthdate-month'
| 'birthdate-year'
| 'cc-csc'
| 'cc-exp'
| 'cc-exp-day'
| 'cc-exp-month'
| 'cc-exp-year'
| 'cc-number'
| 'email'
| 'gender'
| 'name'
| 'name-family'
| 'name-given'
| 'name-middle'
| 'name-middle-initial'
| 'name-prefix'
| 'name-suffix'
| 'password'
| 'password-new'
| 'postal-address'
| 'postal-address-country'
| 'postal-address-extended'
| 'postal-address-extended-postal-code'
| 'postal-address-locality'
| 'postal-address-region'
| 'postal-code'
| 'street-address'
| 'sms-otp'
| 'tel'
| 'tel-country-code'
| 'tel-national'
| 'tel-device'
| 'username'
| 'username-new'
| 'off'
| undefined;
isSecret?: boolean;
isRequired?: boolean;
onChangeText?: ((text: string) => void) | undefined;
}
const TextFieldBase: React.FC<TextFieldBaseProps> = ({ placeholder = '', icon, iconColor, autoComplete = 'off', isSecret = false, isRequired = false, ...props }) => {
const [isPasswordVisible, setPasswordVisible] = useState(!isSecret);
const [isFocused, setFocused] = useState(false);
return (
<View style={styles.container}>
<View style={styles.iconContainerLeft}>
{icon && <Icon name={icon} size={24} color={iconColor ? iconColor : (isFocused ? '#6075F9' : '#454562')} />}
</View>
<TextInput
style={styles.input}
autoComplete={autoComplete}
placeholder={placeholder + (isRequired ? '*' : '')}
placeholderTextColor='#454562'
secureTextEntry={isSecret ? !isPasswordVisible : false}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
{...props}
/>
{isSecret && (
<TouchableOpacity style={styles.iconContainerRight} onPress={() => setPasswordVisible(prevState => !prevState)}>
<Icon name={isPasswordVisible ? 'eye-off' : 'eye'} size={24} color='#454562' />
</TouchableOpacity>
)}
</View>
);
};
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#22222D',
borderRadius: 16,
},
input: {
flex: 1,
color: '#ffffff',
paddingHorizontal: 16 + 24 + 16,
paddingVertical: 16,
outlineStyle: 'none',
},
iconContainerLeft: {
position: 'absolute',
left: 16,
zIndex: 1,
},
iconContainerRight: {
position: 'absolute',
outlineStyle: 'none',
right: 16,
zIndex: 1,
},
});
export default TextFieldBase;

View File

@@ -0,0 +1,183 @@
import React, { useState, useEffect } from 'react';
import { View, TextInput, TouchableOpacity, StyleSheet, Text, Animated } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons'; // Supposons que nous utilisons la bibliothèque Ionicons pour les icônes
interface TextFormFieldProps {
value?: string;
icon?: string;
placeholder?: string;
autoComplete?:
| 'birthdate-day'
| 'birthdate-full'
| 'birthdate-month'
| 'birthdate-year'
| 'cc-csc'
| 'cc-exp'
| 'cc-exp-day'
| 'cc-exp-month'
| 'cc-exp-year'
| 'cc-number'
| 'email'
| 'gender'
| 'name'
| 'name-family'
| 'name-given'
| 'name-middle'
| 'name-middle-initial'
| 'name-prefix'
| 'name-suffix'
| 'password'
| 'password-new'
| 'postal-address'
| 'postal-address-country'
| 'postal-address-extended'
| 'postal-address-extended-postal-code'
| 'postal-address-locality'
| 'postal-address-region'
| 'postal-code'
| 'street-address'
| 'sms-otp'
| 'tel'
| 'tel-country-code'
| 'tel-national'
| 'tel-device'
| 'username'
| 'username-new'
| 'off'
| undefined;
isSecret?: boolean;
isRequired?: boolean;
error?: string;
onChangeText?: ((text: string) => void) | undefined;
}
const ERROR_HEIGHT = 20;
const ERROR_PADDING_TOP = 8;
const TextFormField: React.FC<TextFormFieldProps> = ({ value = '', placeholder = '', icon, autoComplete = 'off', isSecret = false, isRequired = false, error, ...props }) => {
const [isPasswordVisible, setPasswordVisible] = useState(!isSecret);
const [isFocused, setFocused] = useState(false);
const [fieldValue, setFieldValue] = useState(value);
const fadeAnim = React.useRef(new Animated.Value(0)).current; // Initial value for opacity: 0
const heightAnim = React.useRef(new Animated.Value(0)).current; // Initial value for height: 0
const paddingTopAnim = React.useRef(new Animated.Value(0)).current; // Initial value for paddingTop: 0
// Update fieldValue whenever value changes
useEffect(() => {
setFieldValue(value);
}, [value]);
// Animate the error message
useEffect(() => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: error ? 1 : 0,
duration: 500,
useNativeDriver: true
}
),
Animated.timing(
heightAnim,
{
toValue: error ? ERROR_HEIGHT : 0,
duration: 250,
useNativeDriver: false // height cannot be animated using native driver
}
),
Animated.timing(
paddingTopAnim,
{
toValue: error ? ERROR_PADDING_TOP : 0,
duration: 150,
useNativeDriver: false // paddingTop cannot be animated using native driver
}
),
]).start();
}, [error]);
const handleTextChange = (text: string) => {
setFieldValue(text);
if (props.onChangeText) {
props.onChangeText(text);
}
};
return (
<View style={styles.wrapper}>
<View style={[styles.container, error && styles.error, isFocused && styles.containerFocused]}>
<View style={styles.iconContainerLeft}>
{icon && <Icon name={icon} size={24} color={error ? 'red' : (isFocused ? '#6075F9' : '#454562')} />}
</View>
<TextInput
value={fieldValue}
style={styles.input}
autoComplete={autoComplete}
placeholder={placeholder + (isRequired ? '*' : '')}
placeholderTextColor='#454562'
secureTextEntry={isSecret ? !isPasswordVisible : false}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
onChangeText={handleTextChange}
{...props}
/>
{isSecret && (
<TouchableOpacity style={styles.iconContainerRight} onPress={() => setPasswordVisible(prevState => !prevState)}>
<Icon name={isPasswordVisible ? 'eye-off' : 'eye'} size={24} color='#454562' />
</TouchableOpacity>
)}
</View>
<Animated.View style={{...styles.errorContainer, opacity: fadeAnim, height: heightAnim, paddingTop: paddingTopAnim}}>
<Icon name="warning" size={16} color='red' />
<Text style={styles.errorText}>{error}</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: '100%',
},
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#22222D',
borderRadius: 16,
},
error: {
},
containerFocused: {
},
input: {
flex: 1,
color: '#ffffff',
paddingHorizontal: 16 + 24 + 16,
paddingVertical: 16,
outlineStyle: 'none',
},
iconContainerLeft: {
position: 'absolute',
left: 16,
zIndex: 1,
},
iconContainerRight: {
position: 'absolute',
outlineStyle: 'none',
right: 16,
zIndex: 1,
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 40,
},
errorText: {
color: 'red',
fontSize: 12,
marginLeft: 8,
},
});
export default TextFormField;

View File

@@ -0,0 +1,77 @@
import React, { useEffect } from 'react';
import { View, Text, Animated, StyleSheet } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import TextFieldBase, { TextFieldBaseProps } from './TextFieldBase';
interface TextFormFieldProps extends TextFieldBaseProps {
error: string | null;
}
const ERROR_HEIGHT = 20;
const ERROR_PADDING_TOP = 8;
const TextFormField: React.FC<TextFormFieldProps> = ({ error, ...textFieldBaseProps }) => {
const fadeAnim = React.useRef(new Animated.Value(0)).current;
const heightAnim = React.useRef(new Animated.Value(0)).current;
const paddingTopAnim = React.useRef(new Animated.Value(0)).current;
useEffect(() => {
Animated.parallel([
Animated.timing(
fadeAnim,
{
toValue: error ? 1 : 0,
duration: 150,
useNativeDriver: true
}
),
Animated.timing(
heightAnim,
{
toValue: error ? ERROR_HEIGHT : 0,
duration: 250,
useNativeDriver: false
}
),
Animated.timing(
paddingTopAnim,
{
toValue: error ? ERROR_PADDING_TOP : 0,
duration: 250,
useNativeDriver: false
}
),
]).start();
}, [error]);
return (
<View style={styles.wrapper}>
<TextFieldBase
iconColor={error ? '#f7253d' : undefined}
{...textFieldBaseProps}
/>
<Animated.View style={{...styles.errorContainer, opacity: fadeAnim, height: heightAnim, paddingTop: paddingTopAnim}}>
<Icon name="alert-circle" size={16} color='#f7253d' />
<Text style={styles.errorText}>{error}</Text>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
width: '100%',
},
errorContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 16,
},
errorText: {
color: '#f7253d',
fontSize: 12,
marginLeft: 8,
},
});
export default TextFormField;

View File

@@ -5,6 +5,8 @@ import { Translate, translate } from '../../i18n/i18n';
import { string } from 'yup';
import { FormControl, Input, Stack, WarningOutlineIcon, Box, useToast } from 'native-base';
import TextButton from '../TextButton';
import TextFormField from '../UI/TextFormField';
import ButtonBase from '../UI/ButtonBase';
interface SigninFormProps {
onSubmit: (username: string, password: string) => Promise<string>;
@@ -21,8 +23,6 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
error: null as string | null,
},
});
const [submittingForm, setSubmittingForm] = React.useState(false);
const validationSchemas = {
username: string()
.min(3, translate('usernameTooShort'))
@@ -35,18 +35,15 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
};
const toast = useToast();
return (
<Box alignItems="center" style={{ width: '100%' }}>
<Box alignItems="center" style={{ width: '100%', backgroundColor: "#101014" }}>
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl
isRequired
isInvalid={formData.username.error !== null || formData.password.error !== null}
>
<FormControl.Label>
<Translate translationKey="username" />
</FormControl.Label>
<Input
isRequired
type="text"
<TextFormField
error={formData.username.error}
icon='person'
placeholder="Username"
autoComplete="username"
value={formData.username.value}
@@ -59,16 +56,14 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
setFormData({ ...formData, username: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.username.error}
</FormControl.ErrorMessage>
<FormControl.Label>
<Translate translationKey="password" />
</FormControl.Label>
<Input
isRequired
type="password"
/>
<TextFormField
isRequired
isSecret
error={formData.password.error}
icon='lock-closed'
placeholder="Password"
autoComplete="password"
value={formData.password.value}
onChangeText={(t) => {
@@ -81,13 +76,8 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />}>
{formData.password.error}
</FormControl.ErrorMessage>
<TextButton
translate={{ translationKey: 'signInBtn' }}
style={{ marginTop: 10 }}
isLoading={submittingForm}
<ButtonBase
title="Signin"
isDisabled={
formData.password.error !== null ||
formData.username.error !== null ||
@@ -95,24 +85,27 @@ const LoginForm = ({ onSubmit }: SigninFormProps) => {
formData.password.value === ''
}
onPress={async () => {
setSubmittingForm(true);
try {
const resp = await onSubmit(
formData.username.value,
formData.password.value
);
toast.show({ description: resp, colorScheme: 'secondary' });
setSubmittingForm(false);
} catch (e) {
toast.show({
description: e as string,
colorScheme: 'red',
avoidKeyboard: true,
});
setSubmittingForm(false);
}
}}
/>
<ButtonBase
// icon='logo-google'
isOutlined
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title="Signin with google"
/>
</FormControl>
</Stack>
</Box>

View File

@@ -52,6 +52,7 @@
"react-dom": "18.1.0",
"react-i18next": "^11.18.3",
"react-native": "0.70.5",
"react-native-ionicons": "^4.6.5",
"react-native-paper": "^4.12.5",
"react-native-reanimated": "~2.12.0",
"react-native-safe-area-context": "4.4.1",

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

@@ -0,0 +1,164 @@
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 { } from 'native-base';
import SigninForm from '../components/forms/signinform';
import TextButton from '../components/TextButton';
import { useNavigation } from '../Navigation';
import { string } from 'yup';
import { FormControl, Input, Stack, Center, Button, Text, Box, useToast } from 'native-base';
import { TouchableOpacity, Linking, View } from 'react-native'
import TextFormField from '../components/UI/TextFormField';
import LinkBase from '../components/UI/LinkBase';
import SeparatorBase from '../components/UI/SeparatorBase';
import ButtonBase from '../components/UI/ButtonBase';
import { Image, Flex } from 'native-base';
import ImageBanner from '../assets/banner.jpg';
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 (
<Flex direction='row' justifyContent="space-between" style={{ flex: 1, backgroundColor: '#101014'}}>
<Center style={{ flex: 1}}>
<Stack space={4} justifyContent="center" alignContent="center" alignItems="center" style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Text fontSize="4xl" textAlign="center">
Bienvenue !
</Text>
<Text fontSize="xl" textAlign="center">
Continuez avec Google ou entrez vos coordonnées.
</Text>
<ButtonBase
isOutlined
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title="Signin with google"
/>
<SeparatorBase>or</SeparatorBase>
<TextFormField
error={formData.username.error}
icon='person'
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
isRequired
isSecret
error={formData.password.error}
icon='lock-closed'
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 } });
});
}}
/>
<LinkBase onPress={() => console.log('Link clicked!')}>
{translate('forgottenPassword')}
</LinkBase>
<ButtonBase
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,
});
}
}}
/>
<Text>Vous n'avez pas de compte ?</Text>
<LinkBase onPress={() => navigation.navigate('Signup', {})}>
Inscrivez-vous gratuitement
</LinkBase>
</Stack>
</Center>
<View style={{width: '50%', height: '100%', padding: 16}}>
<Image
source={ImageBanner}
alt="banner page"
style={{width: '100%', height: '100%', borderRadius: 8}}
/>
</View>
</Flex>
);
};
export default SigninView;

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

@@ -0,0 +1,214 @@
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 { } from 'native-base';
import SigninForm from '../components/forms/signinform';
import TextButton from '../components/TextButton';
import { useNavigation } from '../Navigation';
import { string } from 'yup';
import { FormControl, Input, Stack, Center, Button, Text, Box, useToast } from 'native-base';
import { TouchableOpacity, Linking, View } from 'react-native'
import TextFormField from '../components/UI/TextFormField';
import LinkBase from '../components/UI/LinkBase';
import SeparatorBase from '../components/UI/SeparatorBase';
import ButtonBase from '../components/UI/ButtonBase';
import { Image, Flex } from 'native-base';
import ImageBanner from '../assets/banner.jpg';
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 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,
},
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 (
<Flex direction='row' justifyContent="space-between" style={{ flex: 1, backgroundColor: '#101014'}}>
<Center style={{ flex: 1}}>
<Stack space={4} justifyContent="center" alignContent="center" alignItems="center" mx="4" style={{ width: '100%', maxWidth: 420, padding: 16 }}>
<Text fontSize="4xl" textAlign="center">Créer un compte</Text>
<Text fontSize="xl" textAlign="center">Apprendre le piano gratuitement et de manière ludique</Text>
<ButtonBase
isOutlined
iconImage='https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/2008px-Google_%22G%22_Logo.svg.png'
title="Signup with google"
/>
<SeparatorBase>or</SeparatorBase>
<TextFormField
error={formData.username.error}
icon='person'
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='mail'
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='lock-closed'
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='lock-closed'
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 },
});
});
}}
/>
<ButtonBase
title="Signup"
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 });
} catch (e) {
toast.show({ description: e as string });
}
}}
/>
<Text>Vous avez déjà un compte ?</Text>
<LinkBase onPress={() => navigation.navigate('Login', {})}>
S'identifier
</LinkBase>
</Stack>
</Center>
<View style={{width: '50%', height: '100%', padding: 16}}>
<Image
source={ImageBanner}
alt="banner page"
style={{width: '100%', height: '100%', borderRadius: 8}}
/>
</View>
</Flex>
);
};
export default SigninView;

View File

@@ -15762,6 +15762,11 @@ react-native-gradle-plugin@^0.70.3:
resolved "https://registry.yarnpkg.com/react-native-gradle-plugin/-/react-native-gradle-plugin-0.70.3.tgz#cbcf0619cbfbddaa9128701aa2d7b4145f9c4fc8"
integrity sha512-oOanj84fJEXUg9FoEAQomA8ISG+DVIrTZ3qF7m69VQUJyOGYyDZmPqKcjvRku4KXlEH6hWO9i4ACLzNBh8gC0A==
react-native-ionicons@^4.6.5:
version "4.6.5"
resolved "https://registry.yarnpkg.com/react-native-ionicons/-/react-native-ionicons-4.6.5.tgz#b792ec8896381e67ff237eba955e38afe7ceb7a4"
integrity sha512-s2Ia7M5t609LE9LWygMj3ALVPUlKhK7R9XcMb67fP4EYJv0oLcwg5pc+8ftv9XXaUuTW/WgL3zJlBYxAvtvMJg==
react-native-iphone-x-helper@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"