Compare commits
3 Commits
clem-fixes
...
redesign
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e71aff8a9 | ||
|
|
a927d9783e | ||
|
|
4de28337a3 |
@@ -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'>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
BIN
front/assets/banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
front/assets/fonts/Lexend-VariableFont_wght.ttf
Normal file
BIN
front/assets/fonts/Lexend-VariableFont_wght.ttf
Normal file
Binary file not shown.
165
front/components/UI/ButtonBase.tsx
Normal file
165
front/components/UI/ButtonBase.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, ActivityIndicator, View, Image } from 'react-native';
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import InteractiveBase from './InteractiveBase';
|
||||
import { Text, useTheme } from 'native-base';
|
||||
// import { BlurView } from 'expo-blur';
|
||||
|
||||
interface ButtonProps {
|
||||
title?: string;
|
||||
onPress?: () => Promise<any>;
|
||||
isDisabled?: boolean;
|
||||
icon?: string;
|
||||
iconImage?: string;
|
||||
isCollapsed?: boolean;
|
||||
type: 'filled' | 'outlined' | 'menu' | 'submenu';
|
||||
}
|
||||
|
||||
const ButtonBase: React.FC<ButtonProps> = ({ title, onPress, isDisabled, icon, iconImage, isCollapsed, type = 'filled'}) => {
|
||||
const { colors } = useTheme();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const styleButton = StyleSheet.create({
|
||||
Default: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: colors.primary[400],
|
||||
},
|
||||
onHover: {
|
||||
scale: 1.02,
|
||||
shadowOpacity: 0.37,
|
||||
shadowRadius: 7.49,
|
||||
elevation: 12,
|
||||
backgroundColor: colors.primary[500],
|
||||
},
|
||||
onPressed: {
|
||||
scale: 0.98,
|
||||
shadowOpacity: 0.23,
|
||||
shadowRadius: 2.62,
|
||||
elevation: 4,
|
||||
backgroundColor: colors.primary[600],
|
||||
},
|
||||
Disabled: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: colors.primary[400],
|
||||
}
|
||||
});
|
||||
|
||||
const styleMenu = StyleSheet.create({
|
||||
Default: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: '#ff0000',
|
||||
},
|
||||
onHover: {
|
||||
scale: 1.01,
|
||||
shadowOpacity: 0.37,
|
||||
shadowRadius: 7.49,
|
||||
elevation: 12,
|
||||
backgroundColor: '#0ff000',
|
||||
},
|
||||
onPressed: {
|
||||
scale: 0.99,
|
||||
shadowOpacity: 0.23,
|
||||
shadowRadius: 2.62,
|
||||
elevation: 4,
|
||||
backgroundColor: '#000',
|
||||
},
|
||||
Disabled: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: '#0000',
|
||||
}
|
||||
});
|
||||
|
||||
const typeToStyleAnimator = {'filled': styleButton,'outlined': styleButton,'menu': styleMenu,'submenu': styleButton};
|
||||
|
||||
return (
|
||||
<InteractiveBase
|
||||
style={styles.container}
|
||||
styleAnimate={typeToStyleAnimator[type]}
|
||||
onPress={async () => {
|
||||
if (onPress && !isDisabled) {
|
||||
setLoading(true);
|
||||
await onPress();
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
isDisabled={isDisabled}
|
||||
isOutlined={type === 'outlined'}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color={type === 'outlined' ? '#6075F9' : '#FFFFFF'} />
|
||||
) : (
|
||||
<View style={styles.content}>
|
||||
{icon && <Ionicons name={icon} size={18} color={type === 'outlined' ? '#ff0000' : '#FFFFFF'} />}
|
||||
{iconImage && <Image source={{uri: iconImage}} style={styles.icon} />}
|
||||
{title && <Text style={styles.text}>{title}</Text>}
|
||||
</View>
|
||||
)}
|
||||
</InteractiveBase>
|
||||
);
|
||||
};
|
||||
|
||||
const styleAnimate = StyleSheet.create({
|
||||
Default: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: '#00ff00',
|
||||
},
|
||||
onHover: {
|
||||
scale: 1.01,
|
||||
shadowOpacity: 0.37,
|
||||
shadowRadius: 7.49,
|
||||
elevation: 12,
|
||||
backgroundColor: '#0000ff',
|
||||
},
|
||||
onPressed: {
|
||||
scale: 0.99,
|
||||
shadowOpacity: 0.23,
|
||||
shadowRadius: 2.62,
|
||||
elevation: 4,
|
||||
backgroundColor: '#ff0000',
|
||||
},
|
||||
Disabled: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: '#000000',
|
||||
}
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
content: {
|
||||
padding: 10,
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
// marginRight: 8,
|
||||
},
|
||||
text: {
|
||||
color: '#fff',
|
||||
marginHorizontal: 8
|
||||
},
|
||||
});
|
||||
|
||||
export default ButtonBase;
|
||||
242
front/components/UI/InteractiveBase.tsx
Normal file
242
front/components/UI/InteractiveBase.tsx
Normal file
@@ -0,0 +1,242 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Animated, StyleSheet, TouchableOpacity, ActivityIndicator, View, Image, StyleProp, ViewStyle } from 'react-native';
|
||||
// import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
// import { Text, useTheme } from 'native-base'
|
||||
// import { BlurView } from '@react-native-community/blur';
|
||||
|
||||
interface InteractiveBaseProps {
|
||||
children?: React.ReactNode;
|
||||
onPress?: () => Promise<any>;
|
||||
isDisabled?: boolean;
|
||||
isOutlined?: boolean;
|
||||
style?: StyleProp<ViewStyle>,
|
||||
styleAnimate: {
|
||||
Default: {
|
||||
scale: number,
|
||||
shadowOpacity: number,
|
||||
shadowRadius: number,
|
||||
elevation: number,
|
||||
backgroundColor: string,
|
||||
},
|
||||
onHover: {
|
||||
scale: number,
|
||||
shadowOpacity: number,
|
||||
shadowRadius: number,
|
||||
elevation: number,
|
||||
backgroundColor: string,
|
||||
},
|
||||
onPressed: {
|
||||
scale: number,
|
||||
shadowOpacity: number,
|
||||
shadowRadius: number,
|
||||
elevation: number,
|
||||
backgroundColor: string,
|
||||
},
|
||||
Disabled: {
|
||||
scale: number,
|
||||
shadowOpacity: number,
|
||||
shadowRadius: number,
|
||||
elevation: number,
|
||||
backgroundColor: string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const InteractiveBase: React.FC<InteractiveBaseProps> = ({ children, onPress, style, styleAnimate, isDisabled = false, isOutlined = false }) => {
|
||||
const scaleAnimator = useRef(new Animated.Value(1)).current;
|
||||
const scaleValue = scaleAnimator.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [styleAnimate.Default.scale, styleAnimate.onHover.scale, styleAnimate.onPressed.scale],
|
||||
});
|
||||
const shadowOpacityAnimator = useRef(new Animated.Value(0)).current;
|
||||
const shadowOpacityValue = shadowOpacityAnimator.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [styleAnimate.Default.shadowOpacity, styleAnimate.onHover.shadowOpacity, styleAnimate.onPressed.shadowOpacity],
|
||||
});
|
||||
const shadowRadiusAnimator = useRef(new Animated.Value(0)).current;
|
||||
const shadowRadiusValue = shadowRadiusAnimator.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [styleAnimate.Default.shadowRadius, styleAnimate.onHover.shadowRadius, styleAnimate.onPressed.shadowRadius],
|
||||
});
|
||||
const elevationAnimator = useRef(new Animated.Value(0)).current;
|
||||
const elevationValue = elevationAnimator.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [styleAnimate.Default.elevation, styleAnimate.onHover.elevation, styleAnimate.onPressed.elevation],
|
||||
});
|
||||
const backgroundColorAnimator = useRef(new Animated.Value(0)).current;
|
||||
const backgroundColorValue = backgroundColorAnimator.interpolate({
|
||||
inputRange: [0, 1, 2],
|
||||
outputRange: [styleAnimate.Default.backgroundColor, styleAnimate.onHover.backgroundColor, styleAnimate.onPressed.backgroundColor],
|
||||
});
|
||||
|
||||
// Mouse Enter
|
||||
const handleMouseEnter = () => {
|
||||
Animated.parallel([
|
||||
Animated.spring(scaleAnimator, {
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(backgroundColorAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowRadiusAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowOpacityAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(elevationAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
// Mouse Down
|
||||
const handlePressIn = () => {
|
||||
Animated.parallel([
|
||||
Animated.spring(scaleAnimator, {
|
||||
toValue: 2,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(backgroundColorAnimator, {
|
||||
toValue: 2,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowRadiusAnimator, {
|
||||
toValue: 2,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowOpacityAnimator, {
|
||||
toValue: 2,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(elevationAnimator, {
|
||||
toValue: 2,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
]).start();
|
||||
};
|
||||
// Mouse Up
|
||||
const handlePressOut = async () => {
|
||||
Animated.parallel([
|
||||
Animated.spring(scaleAnimator, {
|
||||
toValue: 1,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(backgroundColorAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowRadiusAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowOpacityAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(elevationAnimator, {
|
||||
toValue: 1,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
if (onPress && !isDisabled) {
|
||||
await onPress();
|
||||
}
|
||||
}
|
||||
// Mouse Leave
|
||||
const handleMouseLeave = () => {
|
||||
Animated.parallel([
|
||||
Animated.spring(scaleAnimator, {
|
||||
toValue: 0,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(backgroundColorAnimator, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowRadiusAnimator, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(shadowOpacityAnimator, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: false,
|
||||
}),
|
||||
Animated.timing(elevationAnimator, {
|
||||
toValue: 0,
|
||||
duration: 250,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
style,
|
||||
isDisabled ? styleAnimate.Disabled : {
|
||||
backgroundColor: isOutlined ? 'rgba(0,0,0,0.3)' : backgroundColorValue,
|
||||
borderColor: isOutlined ? backgroundColorValue : 'transparent',
|
||||
borderWidth: 2,
|
||||
transform: [{ scale: scaleValue }],
|
||||
shadowOpacity: shadowOpacityValue,
|
||||
shadowRadius: shadowRadiusValue,
|
||||
elevation: elevationValue,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
disabled={isDisabled}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
style={styles.container}
|
||||
>
|
||||
{children}
|
||||
{/* <BlurView
|
||||
style={{
|
||||
width: '420px',
|
||||
height: '50px',
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
|
||||
}}
|
||||
blurType="light"
|
||||
blurAmount={20}
|
||||
blurRadius={5}
|
||||
/> */}
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
export default InteractiveBase;
|
||||
23
front/components/UI/LinkBase.tsx
Normal file
23
front/components/UI/LinkBase.tsx
Normal 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;
|
||||
34
front/components/UI/SeparatorBase.tsx
Normal file
34
front/components/UI/SeparatorBase.tsx
Normal 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;
|
||||
98
front/components/UI/SettingsBase.tsx
Normal file
98
front/components/UI/SettingsBase.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react';
|
||||
import { StyleSheet, ActivityIndicator, View, Image } from 'react-native';
|
||||
import Ionicons from '@expo/vector-icons/Ionicons';
|
||||
import InteractiveBase from './InteractiveBase';
|
||||
import { Text, useTheme } from 'native-base';
|
||||
// import { BlurView } from 'expo-blur';
|
||||
|
||||
interface SettingProps {
|
||||
icon: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
onPress?: () => Promise<any>;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SettingBase: React.FC<SettingProps> = ({ title, description, onPress, icon, children}) => {
|
||||
const styleSetting = StyleSheet.create({
|
||||
Default: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
},
|
||||
onHover: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: 'rgba(32, 32, 40, 0.50)',
|
||||
},
|
||||
onPressed: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
},
|
||||
Disabled: {
|
||||
scale: 1,
|
||||
shadowOpacity: 0.30,
|
||||
shadowRadius: 4.65,
|
||||
elevation: 8,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<InteractiveBase
|
||||
style={[styles.container, {width: '100%'}]}
|
||||
styleAnimate={styleSetting}
|
||||
onPress={async () => {
|
||||
if (onPress) {
|
||||
await onPress();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Ionicons name={icon} size={24} color="#fff"/>
|
||||
<View style={styles.info}>
|
||||
<Text style={styles.text}>{title}</Text>
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
</View>
|
||||
{children}
|
||||
</View>
|
||||
</InteractiveBase>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
borderRadius: 8,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.50)',
|
||||
},
|
||||
content: {
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 20,
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
alignSelf: 'stretch',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
info: {
|
||||
flexDirection: 'column',
|
||||
marginHorizontal: 16,
|
||||
flex: 1,
|
||||
},
|
||||
text: {
|
||||
color: '#fff',
|
||||
fontSize: 16,
|
||||
},
|
||||
description: {
|
||||
color: '#fff',
|
||||
fontSize: 10,
|
||||
},
|
||||
});
|
||||
|
||||
export default SettingBase;
|
||||
109
front/components/UI/TextFieldBase.tsx
Normal file
109
front/components/UI/TextFieldBase.tsx
Normal 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;
|
||||
183
front/components/UI/TextFormField copy.tsx
Normal file
183
front/components/UI/TextFormField copy.tsx
Normal 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;
|
||||
77
front/components/UI/TextFormField.tsx
Normal file
77
front/components/UI/TextFormField.tsx
Normal 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;
|
||||
@@ -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>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"@expo/vector-icons": "^13.0.0",
|
||||
"@motiz88/react-native-midi": "^0.0.5",
|
||||
"@react-native-async-storage/async-storage": "~1.17.3",
|
||||
"@react-native-community/blur": "^4.3.2",
|
||||
"@react-navigation/native": "^6.0.11",
|
||||
"@react-navigation/native-stack": "^6.7.0",
|
||||
"@reduxjs/toolkit": "^1.8.3",
|
||||
@@ -34,6 +35,7 @@
|
||||
"expo": "^47.0.8",
|
||||
"expo-asset": "~8.7.0",
|
||||
"expo-dev-client": "~2.0.1",
|
||||
"expo-linear-gradient": "~12.0.1",
|
||||
"expo-linking": "~3.3.1",
|
||||
"expo-screen-orientation": "~5.0.1",
|
||||
"expo-secure-store": "~12.0.0",
|
||||
@@ -52,6 +54,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",
|
||||
|
||||
181
front/views/SigninView.tsx
Normal file
181
front/views/SigninView.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
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, StyleSheet } 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';
|
||||
import TMPBase from '../components/UI/TMPBase';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import SettingBase from '../components/UI/SettingsBase';
|
||||
|
||||
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
|
||||
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="Signin with google"
|
||||
/>
|
||||
<ButtonBase
|
||||
type='menu'
|
||||
icon='person'
|
||||
title="Menu"
|
||||
/>
|
||||
<SettingBase icon='person' title='title' description='description'>coucou</SettingBase>
|
||||
<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
|
||||
type='outlined'
|
||||
icon='alert'
|
||||
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>
|
||||
<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 SigninView;
|
||||
214
front/views/SignupView.tsx
Normal file
214
front/views/SignupView.tsx
Normal 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;
|
||||
@@ -2884,6 +2884,11 @@
|
||||
dependencies:
|
||||
merge-options "^3.0.4"
|
||||
|
||||
"@react-native-community/blur@^4.3.2":
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/blur/-/blur-4.3.2.tgz#185a2c7dd03ba168cc95069bc4742e9505fd6c6c"
|
||||
integrity sha512-0ID+pyZKdC4RdgC7HePxUQ6JmsbNrgz03u+6SgqYpmBoK/rE+7JffqIw7IEsfoKitLEcRNLGekIBsfwCqiEkew==
|
||||
|
||||
"@react-native-community/cli-clean@^9.2.1":
|
||||
version "9.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@react-native-community/cli-clean/-/cli-clean-9.2.1.tgz#198c5dd39c432efb5374582073065ff75d67d018"
|
||||
@@ -9126,6 +9131,11 @@ expo-keep-awake@~11.0.1:
|
||||
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-11.0.1.tgz#ee354465892a94040ffe09901b85b469e7d54fb3"
|
||||
integrity sha512-44ZjgLE4lnce2d40Pv8xsjMVc6R5GvgHOwZfkLYtGmgYG9TYrEJeEj5UfSeweXPL3pBFhXKfFU8xpGYMaHdP0A==
|
||||
|
||||
expo-linear-gradient@~12.0.1:
|
||||
version "12.0.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-12.0.1.tgz#452f793b0463ddf313aad431552f23acc85f5d64"
|
||||
integrity sha512-TMl/wBTVQOliL4S3DS5Aa3UFfVySr0mdJEHLG6kfBdMCLkr+tfLI2rGyJ+scS7xgMsvhTIaurhf1+Z0sL3aLCg==
|
||||
|
||||
expo-linking@~3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-3.3.1.tgz#253b183321e54cb6fa1a667a53d4594aa88a3357"
|
||||
@@ -15762,6 +15772,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"
|
||||
|
||||
Reference in New Issue
Block a user