Implement custom dark theme for the app and enhance login/register screens with animations. Update dependencies and fix package versions in package.json and package-lock.json.

This commit is contained in:
albivkt
2025-08-06 05:09:12 +03:00
parent 21b4c00d9e
commit d0550acff9
5 changed files with 869 additions and 171 deletions

View File

@ -1,9 +1,23 @@
import React, { useState } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
import { TextInput, Button, Text, Headline, HelperText } from 'react-native-paper';
import { useMutation } from '@apollo/client';
import { LOGIN } from '../graphql/mutations';
import { useAuth } from '../contexts/AuthContext';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
withDelay,
withSequence,
withRepeat,
interpolate,
Easing,
} from 'react-native-reanimated';
const { width: screenWidth } = Dimensions.get('window');
const AnimatedView = Animated.View;
export const LoginScreen = ({ navigation }: any) => {
const [username, setUsername] = useState('');
@ -11,6 +25,32 @@ export const LoginScreen = ({ navigation }: any) => {
const [showPassword, setShowPassword] = useState(false);
const { login } = useAuth();
// Анимации
const translateY = useSharedValue(50);
const opacity = useSharedValue(0);
const scale = useSharedValue(0.9);
const glowAnimation = useSharedValue(0);
const buttonScale = useSharedValue(1);
const inputFocusAnimation1 = useSharedValue(0);
const inputFocusAnimation2 = useSharedValue(0);
useEffect(() => {
// Анимация появления
translateY.value = withSpring(0, { damping: 15, stiffness: 100 });
opacity.value = withTiming(1, { duration: 800 });
scale.value = withSpring(1, { damping: 15, stiffness: 100 });
// Пульсирующее свечение
glowAnimation.value = withRepeat(
withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) })
),
-1,
false
);
}, []);
const [loginMutation, { loading, error }] = useMutation(LOGIN, {
onCompleted: async (data) => {
await login(data.login.access_token, data.login.user);
@ -25,66 +65,173 @@ export const LoginScreen = ({ navigation }: any) => {
loginMutation({ variables: { username, password } });
};
// Анимированные стили
const containerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateY: translateY.value },
{ scale: scale.value }
],
opacity: opacity.value,
};
});
const glowContainerStyle = useAnimatedStyle(() => {
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]);
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]);
return {
shadowOpacity: glowOpacity,
shadowRadius: shadowRadius,
elevation: interpolate(glowAnimation.value, [0, 1], [5, 15]),
};
});
const buttonAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: buttonScale.value }],
};
});
const createInputAnimatedStyle = (focusAnimation: any) => {
return useAnimatedStyle(() => {
const borderWidth = interpolate(focusAnimation.value, [0, 1], [1, 2]);
const shadowOpacity = interpolate(focusAnimation.value, [0, 1], [0, 0.6]);
return {
borderWidth: borderWidth,
shadowOpacity: shadowOpacity,
elevation: interpolate(focusAnimation.value, [0, 1], [2, 8]),
};
});
};
const inputStyle1 = createInputAnimatedStyle(inputFocusAnimation1);
const inputStyle2 = createInputAnimatedStyle(inputFocusAnimation2);
const handleButtonPressIn = () => {
buttonScale.value = withSpring(0.95);
};
const handleButtonPressOut = () => {
buttonScale.value = withSpring(1);
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.content}>
<Headline style={styles.title}>Вход в Prism</Headline>
<Animated.View style={[styles.content, containerAnimatedStyle]}>
<Animated.View style={[styles.glowContainer, glowContainerStyle]}>
<Headline style={styles.title}>Вход в Prism</Headline>
<Text style={styles.subtitle}>Добро пожаловать обратно</Text>
</Animated.View>
<TextInput
label="Имя пользователя"
value={username}
onChangeText={setUsername}
mode="outlined"
style={styles.input}
autoCapitalize="none"
disabled={loading}
/>
<AnimatedView style={inputStyle1}>
<TextInput
label="Имя пользователя"
value={username}
onChangeText={setUsername}
mode="outlined"
style={styles.input}
autoCapitalize="none"
disabled={loading}
theme={{
colors: {
primary: '#9333ea',
placeholder: '#a855f7',
text: '#ffffff',
background: '#1a1a2e',
outline: '#7c3aed',
}
}}
onFocus={() => {
inputFocusAnimation1.value = withSpring(1);
}}
onBlur={() => {
inputFocusAnimation1.value = withSpring(0);
}}
/>
</AnimatedView>
<TextInput
label="Пароль"
value={password}
onChangeText={setPassword}
mode="outlined"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
<AnimatedView style={inputStyle2}>
<TextInput
label="Пароль"
value={password}
onChangeText={setPassword}
mode="outlined"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
theme={{
colors: {
primary: '#9333ea',
placeholder: '#a855f7',
text: '#ffffff',
background: '#1a1a2e',
outline: '#7c3aed',
}
}}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
color="#a855f7"
/>
}
onFocus={() => {
inputFocusAnimation2.value = withSpring(1);
}}
onBlur={() => {
inputFocusAnimation2.value = withSpring(0);
}}
/>
</AnimatedView>
{error && (
<HelperText type="error" visible={true}>
<HelperText type="error" visible={true} style={styles.errorText}>
{error.message}
</HelperText>
)}
<Button
mode="contained"
onPress={handleLogin}
loading={loading}
disabled={loading || !username || !password}
style={styles.button}
>
Войти
</Button>
<AnimatedView style={buttonAnimatedStyle}>
<Button
mode="contained"
onPress={handleLogin}
onPressIn={handleButtonPressIn}
onPressOut={handleButtonPressOut}
loading={loading}
disabled={loading || !username || !password}
style={styles.button}
contentStyle={styles.buttonContent}
labelStyle={styles.buttonLabel}
theme={{
colors: {
primary: '#9333ea',
}
}}
>
Войти
</Button>
</AnimatedView>
<Button
mode="text"
onPress={() => navigation.navigate('Register')}
disabled={loading}
style={styles.linkButton}
labelStyle={styles.linkButtonLabel}
theme={{
colors: {
primary: '#a855f7',
}
}}
>
Нет аккаунта? Зарегистрироваться
</Button>
</View>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
);
@ -93,11 +240,12 @@ export const LoginScreen = ({ navigation }: any) => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
backgroundColor: '#0a0a0f',
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
paddingVertical: 40,
},
content: {
padding: 20,
@ -105,18 +253,90 @@ const styles = StyleSheet.create({
width: '100%',
alignSelf: 'center',
},
glowContainer: {
marginBottom: 40,
padding: 20,
borderRadius: 20,
backgroundColor: '#1a1a2e',
// iOS тени
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 0.5,
shadowRadius: 20,
// Android тень
elevation: 10,
},
title: {
textAlign: 'center',
marginBottom: 30,
marginBottom: 10,
fontSize: 32,
fontWeight: 'bold',
color: '#ffffff',
textShadowColor: '#9333ea',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 10,
},
subtitle: {
textAlign: 'center',
fontSize: 16,
color: '#a855f7',
fontStyle: 'italic',
},
input: {
marginBottom: 15,
marginBottom: 20,
backgroundColor: '#1a1a2e',
borderRadius: 12,
// iOS тени
shadowColor: '#7c3aed',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 6,
// Android тень
elevation: 5,
},
button: {
marginTop: 10,
marginTop: 20,
marginBottom: 10,
borderRadius: 12,
backgroundColor: '#9333ea',
// iOS тени для кнопки
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.6,
shadowRadius: 15,
// Android тень
elevation: 10,
},
buttonContent: {
paddingVertical: 8,
},
buttonLabel: {
fontSize: 18,
fontWeight: 'bold',
letterSpacing: 1,
},
linkButton: {
marginTop: 10,
},
linkButtonLabel: {
fontSize: 16,
color: '#a855f7',
},
errorText: {
color: '#ef4444',
textAlign: 'center',
marginBottom: 10,
textShadowColor: '#ef4444',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 5,
},
});

View File

@ -1,9 +1,23 @@
import React, { useState } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView } from 'react-native';
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
import { TextInput, Button, Text, Headline, HelperText } from 'react-native-paper';
import { useMutation } from '@apollo/client';
import { REGISTER } from '../graphql/mutations';
import { useAuth } from '../contexts/AuthContext';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSpring,
withDelay,
withSequence,
withRepeat,
interpolate,
Easing,
} from 'react-native-reanimated';
const { width: screenWidth } = Dimensions.get('window');
const AnimatedView = Animated.View;
export const RegisterScreen = ({ navigation }: any) => {
const [username, setUsername] = useState('');
@ -13,6 +27,34 @@ export const RegisterScreen = ({ navigation }: any) => {
const [showPassword, setShowPassword] = useState(false);
const { login } = useAuth();
// Анимации
const translateY = useSharedValue(50);
const opacity = useSharedValue(0);
const scale = useSharedValue(0.9);
const glowAnimation = useSharedValue(0);
const buttonScale = useSharedValue(1);
const inputFocusAnimation1 = useSharedValue(0);
const inputFocusAnimation2 = useSharedValue(0);
const inputFocusAnimation3 = useSharedValue(0);
const inputFocusAnimation4 = useSharedValue(0);
useEffect(() => {
// Анимация появления
translateY.value = withSpring(0, { damping: 15, stiffness: 100 });
opacity.value = withTiming(1, { duration: 800 });
scale.value = withSpring(1, { damping: 15, stiffness: 100 });
// Пульсирующее свечение
glowAnimation.value = withRepeat(
withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) })
),
-1,
false
);
}, []);
const [registerMutation, { loading, error }] = useMutation(REGISTER, {
onCompleted: async (data) => {
await login(data.register.access_token, data.register.user);
@ -29,94 +71,238 @@ export const RegisterScreen = ({ navigation }: any) => {
const passwordsMatch = password === confirmPassword || confirmPassword === '';
// Анимированные стили
const containerAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateY: translateY.value },
{ scale: scale.value }
],
opacity: opacity.value,
};
});
const glowContainerStyle = useAnimatedStyle(() => {
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]);
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]);
return {
shadowOpacity: glowOpacity,
shadowRadius: shadowRadius,
elevation: interpolate(glowAnimation.value, [0, 1], [5, 15]),
};
});
const buttonAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [{ scale: buttonScale.value }],
};
});
const createInputAnimatedStyle = (focusAnimation: any) => {
return useAnimatedStyle(() => {
const borderWidth = interpolate(focusAnimation.value, [0, 1], [1, 2]);
const shadowOpacity = interpolate(focusAnimation.value, [0, 1], [0, 0.6]);
return {
borderWidth: borderWidth,
shadowOpacity: shadowOpacity,
elevation: interpolate(focusAnimation.value, [0, 1], [2, 8]),
};
});
};
const inputStyle1 = createInputAnimatedStyle(inputFocusAnimation1);
const inputStyle2 = createInputAnimatedStyle(inputFocusAnimation2);
const inputStyle3 = createInputAnimatedStyle(inputFocusAnimation3);
const inputStyle4 = createInputAnimatedStyle(inputFocusAnimation4);
const handleButtonPressIn = () => {
buttonScale.value = withSpring(0.95);
};
const handleButtonPressOut = () => {
buttonScale.value = withSpring(1);
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView contentContainerStyle={styles.scrollContent}>
<View style={styles.content}>
<Headline style={styles.title}>Регистрация в Prism</Headline>
<Animated.View style={[styles.content, containerAnimatedStyle]}>
<Animated.View style={[styles.glowContainer, glowContainerStyle]}>
<Headline style={styles.title}>Регистрация в Prism</Headline>
<Text style={styles.subtitle}>Присоединяйтесь к будущему</Text>
</Animated.View>
<TextInput
label="Имя пользователя"
value={username}
onChangeText={setUsername}
mode="outlined"
style={styles.input}
autoCapitalize="none"
disabled={loading}
/>
<AnimatedView style={inputStyle1}>
<TextInput
label="Имя пользователя"
value={username}
onChangeText={setUsername}
mode="outlined"
style={styles.input}
autoCapitalize="none"
disabled={loading}
theme={{
colors: {
primary: '#9333ea',
placeholder: '#a855f7',
text: '#ffffff',
background: '#1a1a2e',
outline: '#7c3aed',
}
}}
onFocus={() => {
inputFocusAnimation1.value = withSpring(1);
}}
onBlur={() => {
inputFocusAnimation1.value = withSpring(0);
}}
/>
</AnimatedView>
<TextInput
label="Email"
value={email}
onChangeText={setEmail}
mode="outlined"
style={styles.input}
keyboardType="email-address"
autoCapitalize="none"
disabled={loading}
/>
<AnimatedView style={inputStyle2}>
<TextInput
label="Email"
value={email}
onChangeText={setEmail}
mode="outlined"
style={styles.input}
keyboardType="email-address"
autoCapitalize="none"
disabled={loading}
theme={{
colors: {
primary: '#9333ea',
placeholder: '#a855f7',
text: '#ffffff',
background: '#1a1a2e',
outline: '#7c3aed',
}
}}
onFocus={() => {
inputFocusAnimation2.value = withSpring(1);
}}
onBlur={() => {
inputFocusAnimation2.value = withSpring(0);
}}
/>
</AnimatedView>
<TextInput
label="Пароль"
value={password}
onChangeText={setPassword}
mode="outlined"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
/>
}
/>
<AnimatedView style={inputStyle3}>
<TextInput
label="Пароль"
value={password}
onChangeText={setPassword}
mode="outlined"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
theme={{
colors: {
primary: '#9333ea',
placeholder: '#a855f7',
text: '#ffffff',
background: '#1a1a2e',
outline: '#7c3aed',
}
}}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
color="#a855f7"
/>
}
onFocus={() => {
inputFocusAnimation3.value = withSpring(1);
}}
onBlur={() => {
inputFocusAnimation3.value = withSpring(0);
}}
/>
</AnimatedView>
<TextInput
label="Подтвердите пароль"
value={confirmPassword}
onChangeText={setConfirmPassword}
mode="outlined"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
error={!passwordsMatch}
/>
<AnimatedView style={inputStyle4}>
<TextInput
label="Подтвердите пароль"
value={confirmPassword}
onChangeText={setConfirmPassword}
mode="outlined"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
error={!passwordsMatch}
theme={{
colors: {
primary: '#9333ea',
placeholder: '#a855f7',
text: '#ffffff',
background: '#1a1a2e',
outline: '#7c3aed',
error: '#ef4444',
}
}}
onFocus={() => {
inputFocusAnimation4.value = withSpring(1);
}}
onBlur={() => {
inputFocusAnimation4.value = withSpring(0);
}}
/>
</AnimatedView>
{!passwordsMatch && (
<HelperText type="error" visible={true}>
<HelperText type="error" visible={true} style={styles.errorText}>
Пароли не совпадают
</HelperText>
)}
{error && (
<HelperText type="error" visible={true}>
<HelperText type="error" visible={true} style={styles.errorText}>
{error.message}
</HelperText>
)}
<Button
mode="contained"
onPress={handleRegister}
loading={loading}
disabled={loading || !username || !email || !password || !passwordsMatch}
style={styles.button}
>
Зарегистрироваться
</Button>
<AnimatedView style={buttonAnimatedStyle}>
<Button
mode="contained"
onPress={handleRegister}
onPressIn={handleButtonPressIn}
onPressOut={handleButtonPressOut}
loading={loading}
disabled={loading || !username || !email || !password || !passwordsMatch}
style={styles.button}
contentStyle={styles.buttonContent}
labelStyle={styles.buttonLabel}
theme={{
colors: {
primary: '#9333ea',
}
}}
>
Зарегистрироваться
</Button>
</AnimatedView>
<Button
mode="text"
onPress={() => navigation.navigate('Login')}
disabled={loading}
style={styles.linkButton}
labelStyle={styles.linkButtonLabel}
theme={{
colors: {
primary: '#a855f7',
}
}}
>
Уже есть аккаунт? Войти
</Button>
</View>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
);
@ -125,11 +311,12 @@ export const RegisterScreen = ({ navigation }: any) => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
backgroundColor: '#0a0a0f',
},
scrollContent: {
flexGrow: 1,
justifyContent: 'center',
paddingVertical: 40,
},
content: {
padding: 20,
@ -137,18 +324,90 @@ const styles = StyleSheet.create({
width: '100%',
alignSelf: 'center',
},
glowContainer: {
marginBottom: 40,
padding: 20,
borderRadius: 20,
backgroundColor: '#1a1a2e',
// iOS тени
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 0.5,
shadowRadius: 20,
// Android тень
elevation: 10,
},
title: {
textAlign: 'center',
marginBottom: 30,
marginBottom: 10,
fontSize: 32,
fontWeight: 'bold',
color: '#ffffff',
textShadowColor: '#9333ea',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 10,
},
subtitle: {
textAlign: 'center',
fontSize: 16,
color: '#a855f7',
fontStyle: 'italic',
},
input: {
marginBottom: 15,
marginBottom: 20,
backgroundColor: '#1a1a2e',
borderRadius: 12,
// iOS тени
shadowColor: '#7c3aed',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 6,
// Android тень
elevation: 5,
},
button: {
marginTop: 10,
marginTop: 20,
marginBottom: 10,
borderRadius: 12,
backgroundColor: '#9333ea',
// iOS тени для кнопки
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.6,
shadowRadius: 15,
// Android тень
elevation: 10,
},
buttonContent: {
paddingVertical: 8,
},
buttonLabel: {
fontSize: 18,
fontWeight: 'bold',
letterSpacing: 1,
},
linkButton: {
marginTop: 10,
},
linkButtonLabel: {
fontSize: 16,
color: '#a855f7',
},
errorText: {
color: '#ef4444',
textAlign: 'center',
marginBottom: 10,
textShadowColor: '#ef4444',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 5,
},
});