Compare commits

...

2 Commits

Author SHA1 Message Date
6a141d25b7 Обновил дизайн на черно-серый 2025-08-06 06:14:55 +03:00
020682854d 1212 2025-08-06 06:04:23 +03:00
8 changed files with 1507 additions and 494 deletions

View File

@ -1,12 +1,65 @@
import React from 'react'; import React from 'react';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { Platform } from 'react-native';
import { ApolloProvider } from '@apollo/client'; import { ApolloProvider } from '@apollo/client';
import { Provider as PaperProvider } from 'react-native-paper'; import { Provider as PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { apolloClient } from './src/services/apollo-client'; import { apolloClient } from './src/services/apollo-client';
import { AuthProvider } from './src/contexts/AuthContext'; import { AuthProvider } from './src/contexts/AuthContext';
import { AppNavigator } from './src/navigation/AppNavigator'; import { AppNavigator } from './src/navigation/AppNavigator';
<<<<<<< HEAD
// Современная черно-серая тема
const theme = {
...MD3DarkTheme,
colors: {
...MD3DarkTheme.colors,
primary: '#ffffff',
secondary: '#b3b3b3',
tertiary: '#808080',
background: '#0a0a0a',
surface: '#1a1a1a',
surfaceVariant: '#2d2d2d',
onSurface: '#ffffff',
onSurfaceVariant: '#e5e5e5',
onPrimary: '#000000',
elevation: {
level0: 'transparent',
level1: '#1a1a1a',
level2: '#242424',
level3: '#2e2e2e',
level4: '#383838',
level5: '#424242',
},
outline: '#666666',
outlineVariant: '#4d4d4d',
error: '#ff6b6b',
inverseSurface: '#e6e6e6',
inverseOnSurface: '#1a1a1a',
inversePrimary: '#000000',
backdrop: 'rgba(0, 0, 0, 0.8)',
notification: '#ffffff',
card: '#1a1a1a',
text: '#ffffff',
border: '#333333',
placeholder: '#808080',
},
roundness: 16,
fonts: configureFonts({
customVariant: {
fontFamily: Platform.select({
ios: 'System',
android: 'Roboto',
default: 'sans-serif',
}),
fontWeight: '500',
letterSpacing: 0.5,
},
}),
};
=======
import { theme } from './src/theme'; import { theme } from './src/theme';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
export default function App() { export default function App() {
return ( return (

View File

@ -16,6 +16,7 @@
"@react-navigation/native-stack": "^7.3.24", "@react-navigation/native-stack": "^7.3.24",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"expo": "~53.0.20", "expo": "~53.0.20",
"expo-linear-gradient": "^14.1.5",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"react": "19.0.0", "react": "19.0.0",
@ -27,6 +28,7 @@
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"react-native-svg": "15.11.2",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-native-web": "^0.20.0" "react-native-web": "^0.20.0"
}, },
@ -3425,6 +3427,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC"
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@ -3997,6 +4005,56 @@
"hyphenate-style-name": "^1.0.3" "hyphenate-style-name": "^1.0.3"
} }
}, },
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0",
"css-what": "^6.1.0",
"domhandler": "^5.0.2",
"domutils": "^3.0.1",
"nth-check": "^2.0.1"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
"license": "MIT",
"dependencies": {
"mdn-data": "2.0.14",
"source-map": "^0.6.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/css-tree/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">= 6"
},
"funding": {
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -4110,6 +4168,61 @@
"node": ">=0.10" "node": ">=0.10"
} }
}, },
"node_modules/dom-serializer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
"license": "MIT",
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.2",
"entities": "^4.2.0"
},
"funding": {
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
}
},
"node_modules/domelementtype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fb55"
}
],
"license": "BSD-2-Clause"
},
"node_modules/domhandler": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
"license": "BSD-2-Clause",
"dependencies": {
"domelementtype": "^2.3.0"
},
"engines": {
"node": ">= 4"
},
"funding": {
"url": "https://github.com/fb55/domhandler?sponsor=1"
}
},
"node_modules/domutils": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
"license": "BSD-2-Clause",
"dependencies": {
"dom-serializer": "^2.0.0",
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3"
},
"funding": {
"url": "https://github.com/fb55/domutils?sponsor=1"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.4.7", "version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -4170,6 +4283,18 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-editor": { "node_modules/env-editor": {
"version": "0.4.2", "version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -4371,6 +4496,17 @@
"react": "*" "react": "*"
} }
}, },
"node_modules/expo-linear-gradient": {
"version": "14.1.5",
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz",
"integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-modules-autolinking": { "node_modules/expo-modules-autolinking": {
"version": "2.1.14", "version": "2.1.14",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz", "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz",
@ -5733,6 +5869,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/mdn-data": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
"license": "CC0-1.0"
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -6343,6 +6485,18 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/nth-check": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
"license": "BSD-2-Clause",
"dependencies": {
"boolbase": "^1.0.0"
},
"funding": {
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
"node_modules/nullthrows": { "node_modules/nullthrows": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -7221,6 +7375,21 @@
"react-native": "*" "react-native": "*"
} }
}, },
"node_modules/react-native-svg": {
"version": "15.11.2",
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
"integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==",
"license": "MIT",
"dependencies": {
"css-select": "^5.1.0",
"css-tree": "^1.1.3",
"warn-once": "0.1.1"
},
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-vector-icons": { "node_modules/react-native-vector-icons": {
"version": "10.3.0", "version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",

View File

@ -10,15 +10,18 @@
}, },
"dependencies": { "dependencies": {
"@apollo/client": "^3.13.9", "@apollo/client": "^3.13.9",
"@expo/metro-runtime": "~5.0.4",
"@react-native-async-storage/async-storage": "2.1.2", "@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/bottom-tabs": "^7.4.5", "@react-navigation/bottom-tabs": "^7.4.5",
"@react-navigation/native": "^7.1.17", "@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.24", "@react-navigation/native-stack": "^7.3.24",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"expo": "~53.0.20", "expo": "~53.0.20",
"expo-linear-gradient": "^14.1.5",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~2.2.3",
"graphql": "^16.11.0", "graphql": "^16.11.0",
"react": "19.0.0", "react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5", "react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0", "react-native-gesture-handler": "~2.24.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5", "react-native-keyboard-aware-scroll-view": "^0.9.5",
@ -27,9 +30,8 @@
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.11.1",
"react-native-vector-icons": "^10.3.0", "react-native-vector-icons": "^10.3.0",
"react-dom": "19.0.0",
"react-native-web": "^0.20.0", "react-native-web": "^0.20.0",
"@expo/metro-runtime": "~5.0.4" "react-native-svg": "15.11.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",

View File

@ -0,0 +1,252 @@
import React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withTiming,
withSequence,
interpolate,
Easing,
} from 'react-native-reanimated';
const { width, height } = Dimensions.get('window');
interface BackgroundDesignProps {
variant?: 'default' | 'login' | 'chat';
children?: React.ReactNode;
}
export const BackgroundDesign: React.FC<BackgroundDesignProps> = ({
variant = 'default',
children
}) => {
const floatAnimation = useSharedValue(0);
const rotateAnimation = useSharedValue(0);
const pulseAnimation = useSharedValue(0);
React.useEffect(() => {
// Плавное движение вверх-вниз
floatAnimation.value = withRepeat(
withSequence(
withTiming(1, { duration: 4000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 4000, easing: Easing.inOut(Easing.ease) })
),
-1,
false
);
// Вращение элементов
rotateAnimation.value = withRepeat(
withTiming(360, { duration: 30000, easing: Easing.linear }),
-1,
false
);
// Пульсация
pulseAnimation.value = withRepeat(
withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) })
),
-1,
false
);
}, []);
const floatingStyle = useAnimatedStyle(() => {
const translateY = interpolate(floatAnimation.value, [0, 1], [0, -30]);
return {
transform: [{ translateY }],
};
});
const rotatingStyle = useAnimatedStyle(() => {
return {
transform: [{ rotate: `${rotateAnimation.value}deg` }],
};
});
const pulsingStyle = useAnimatedStyle(() => {
const scale = interpolate(pulseAnimation.value, [0, 1], [1, 1.1]);
const opacity = interpolate(pulseAnimation.value, [0, 1], [0.3, 0.6]);
return {
transform: [{ scale }],
opacity,
};
});
return (
<View style={styles.container}>
{/* Основной градиентный фон */}
<LinearGradient
colors={['#0a0a0a', '#1a1a1a', '#0f0f0f']}
style={StyleSheet.absoluteFillObject}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
{/* Декоративные элементы */}
<View style={styles.decorativeElements}>
{/* Большой круг с градиентом слева вверху */}
<Animated.View style={[styles.circle1, pulsingStyle]}>
<LinearGradient
colors={['rgba(255,255,255,0.03)', 'rgba(255,255,255,0.01)']}
style={styles.circleGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
</Animated.View>
{/* Плавающий элемент справа */}
<Animated.View style={[styles.floatingElement1, floatingStyle]}>
<LinearGradient
colors={['rgba(255,255,255,0.05)', 'rgba(255,255,255,0.02)']}
style={styles.elementGradient}
/>
</Animated.View>
{/* Вращающийся квадрат */}
<Animated.View style={[styles.rotatingSquare, rotatingStyle]}>
<View style={styles.squareInner} />
</Animated.View>
{/* Сетка точек для login варианта */}
{variant === 'login' && (
<View style={styles.dotsGrid}>
{Array.from({ length: 10 }).map((_, i) =>
Array.from({ length: 15 }).map((_, j) => (
<View
key={`${i}-${j}`}
style={[
styles.dot,
{
left: i * (width / 9),
top: j * (height / 14),
opacity: 0.05 + Math.random() * 0.05,
}
]}
/>
))
)}
</View>
)}
{/* Градиентные блики */}
<View style={styles.glowContainer}>
<LinearGradient
colors={['rgba(255,255,255,0.1)', 'transparent']}
style={[styles.glow1]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
/>
<LinearGradient
colors={['rgba(255,255,255,0.08)', 'transparent']}
style={[styles.glow2]}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
/>
</View>
</View>
{/* Контент поверх фона */}
<View style={styles.contentContainer}>
{children}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#0a0a0a',
},
decorativeElements: {
...StyleSheet.absoluteFillObject,
overflow: 'hidden',
},
contentContainer: {
flex: 1,
zIndex: 1,
},
circle1: {
position: 'absolute',
top: -width * 0.2,
left: -width * 0.2,
width: width * 0.6,
height: width * 0.6,
borderRadius: width * 0.3,
overflow: 'hidden',
},
circleGradient: {
flex: 1,
},
floatingElement1: {
position: 'absolute',
top: height * 0.2,
right: width * 0.1,
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(255,255,255,0.02)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.05)',
overflow: 'hidden',
},
elementGradient: {
flex: 1,
},
rotatingSquare: {
position: 'absolute',
bottom: height * 0.15,
left: width * 0.15,
width: 60,
height: 60,
backgroundColor: 'transparent',
borderWidth: 2,
borderColor: 'rgba(255,255,255,0.1)',
transform: [{ rotate: '45deg' }],
},
squareInner: {
flex: 1,
margin: 10,
backgroundColor: 'transparent',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.05)',
},
dotsGrid: {
position: 'absolute',
top: 0,
left: 0,
width: width,
height: height,
},
dot: {
position: 'absolute',
width: 2,
height: 2,
borderRadius: 1,
backgroundColor: '#666666',
},
glowContainer: {
...StyleSheet.absoluteFillObject,
},
glow1: {
position: 'absolute',
top: height * 0.1,
right: -width * 0.2,
width: width * 0.8,
height: width * 0.8,
borderRadius: width * 0.4,
},
glow2: {
position: 'absolute',
bottom: -height * 0.1,
left: -width * 0.1,
width: width * 0.7,
height: width * 0.7,
borderRadius: width * 0.35,
},
});

View File

@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { View, StyleSheet, FlatList, KeyboardAvoidingView, Platform } from 'react-native'; import { View, StyleSheet, FlatList, KeyboardAvoidingView, Platform, TouchableOpacity } from 'react-native';
import { TextInput, IconButton, Text, Avatar, Surface, Menu } from 'react-native-paper'; import { TextInput, IconButton, Text, Avatar, Surface, Menu } from 'react-native-paper';
import { useQuery, useMutation, useSubscription } from '@apollo/client'; import { useQuery, useMutation, useSubscription } from '@apollo/client';
import { GET_MESSAGES } from '../graphql/queries'; import { GET_MESSAGES } from '../graphql/queries';
@ -9,6 +9,14 @@ import { Message } from '../types';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ru } from 'date-fns/locale'; import { ru } from 'date-fns/locale';
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
FadeInDown,
FadeOutDown,
Layout,
SlideInRight,
SlideInLeft,
} from 'react-native-reanimated';
export const ChatScreen = ({ route }: any) => { export const ChatScreen = ({ route }: any) => {
const { conversationId, title } = route.params; const { conversationId, title } = route.params;
@ -61,7 +69,6 @@ export const ChatScreen = ({ route }: any) => {
variables: { conversationId }, variables: { conversationId },
onData: ({ data }) => { onData: ({ data }) => {
if (data?.data?.messageAdded && data.data.messageAdded.sender.id !== user?.id) { if (data?.data?.messageAdded && data.data.messageAdded.sender.id !== user?.id) {
// Сообщение от другого пользователя
flatListRef.current?.scrollToEnd(); flatListRef.current?.scrollToEnd();
} }
}, },
@ -94,35 +101,60 @@ export const ChatScreen = ({ route }: any) => {
} }
}; };
const renderMessage = ({ item }: { item: Message }) => { const renderMessage = ({ item, index }: { item: Message; index: number }) => {
const isOwnMessage = item.sender.id === user?.id; const isOwnMessage = item.sender.id === user?.id;
const messageTime = format(new Date(item.createdAt), 'HH:mm', { locale: ru }); const messageTime = format(new Date(item.createdAt), 'HH:mm', { locale: ru });
return ( return (
<View style={[styles.messageContainer, isOwnMessage && styles.ownMessageContainer]}> <Animated.View
entering={isOwnMessage ? SlideInRight.delay(index * 50) : SlideInLeft.delay(index * 50)}
exiting={FadeOutDown}
layout={Layout.springify()}
style={[styles.messageContainer, isOwnMessage && styles.ownMessageContainer]}
>
{!isOwnMessage && ( {!isOwnMessage && (
<Avatar.Text <Avatar.Text
size={36} size={40}
label={item.sender.username.charAt(0).toUpperCase()} label={item.sender.username.charAt(0).toUpperCase()}
style={styles.avatar} style={styles.avatar}
labelStyle={styles.avatarLabel}
theme={{
colors: {
primary: '#2d2d2d',
}
}}
/> />
)} )}
<Surface <View style={[styles.messageBubbleContainer, isOwnMessage && styles.ownMessageBubbleContainer]}>
style={[styles.messageBubble, isOwnMessage && styles.ownMessageBubble]} {isOwnMessage ? (
elevation={1} <LinearGradient
> colors={['#ffffff', '#f0f0f0']}
{!isOwnMessage && ( style={styles.messageBubble}
<Text variant="labelSmall" style={styles.senderName}> start={{ x: 0, y: 0 }}
{item.sender.username} end={{ x: 1, y: 1 }}
</Text> >
<Text style={[styles.messageText, styles.ownMessageText]}>
{item.content}
</Text>
<Text variant="bodySmall" style={[styles.messageTime, styles.ownMessageTime]}>
{messageTime}
{item.isEdited && ' • изменено'}
</Text>
</LinearGradient>
) : (
<Surface style={[styles.messageBubble, styles.otherMessageBubble]} elevation={1}>
<Text variant="labelSmall" style={styles.senderName}>
{item.sender.username}
</Text>
<Text style={styles.messageText}>
{item.content}
</Text>
<Text variant="bodySmall" style={styles.messageTime}>
{messageTime}
{item.isEdited && ' • изменено'}
</Text>
</Surface>
)} )}
<Text style={[styles.messageText, isOwnMessage && styles.ownMessageText]}>
{item.content}
</Text>
<Text variant="bodySmall" style={[styles.messageTime, isOwnMessage && styles.ownMessageTime]}>
{messageTime}
{item.isEdited && ' • изменено'}
</Text>
{isOwnMessage && ( {isOwnMessage && (
<Menu <Menu
visible={menuVisible && selectedMessage === item.id} visible={menuVisible && selectedMessage === item.id}
@ -131,78 +163,119 @@ export const ChatScreen = ({ route }: any) => {
setSelectedMessage(null); setSelectedMessage(null);
}} }}
anchor={ anchor={
<IconButton <TouchableOpacity
icon="dots-vertical"
size={16}
onPress={() => { onPress={() => {
setSelectedMessage(item.id); setSelectedMessage(item.id);
setMenuVisible(true); setMenuVisible(true);
}} }}
style={styles.menuButton} style={styles.menuButton}
/> >
<IconButton
icon="dots-vertical"
size={20}
iconColor="#666666"
/>
</TouchableOpacity>
} }
contentStyle={styles.menuContent}
> >
<Menu.Item onPress={() => handleDeleteMessage(item.id)} title="Удалить" /> <Menu.Item
onPress={() => handleDeleteMessage(item.id)}
title="Удалить"
titleStyle={styles.menuItemText}
/>
</Menu> </Menu>
)} )}
</Surface> </View>
</View> </Animated.View>
); );
}; };
if (loading && !data) { if (loading && !data) {
return ( return (
<View style={styles.centerContainer}> <View style={styles.loadingContainer}>
<Text>Загрузка сообщений...</Text> <Text style={styles.loadingText}>Загрузка сообщений...</Text>
</View> </View>
); );
} }
return ( return (
<KeyboardAvoidingView <View style={styles.container}>
style={styles.container} <LinearGradient
behavior={Platform.OS === 'ios' ? 'padding' : undefined} colors={['#0a0a0a', '#1a1a1a']}
keyboardVerticalOffset={90} style={StyleSheet.absoluteFillObject}
>
<FlatList
ref={flatListRef}
data={data?.messages || []}
renderItem={renderMessage}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.messagesList}
onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
/> />
<View style={styles.inputContainer}> <KeyboardAvoidingView
<TextInput style={styles.keyboardAvoidingView}
value={message} behavior={Platform.OS === 'ios' ? 'padding' : undefined}
onChangeText={setMessage} keyboardVerticalOffset={90}
placeholder="Введите сообщение..." >
mode="outlined" <FlatList
style={styles.input} ref={flatListRef}
multiline data={data?.messages || []}
maxLength={1000} renderItem={renderMessage}
right={ keyExtractor={(item) => item.id}
<TextInput.Icon contentContainerStyle={styles.messagesList}
icon="send" onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
onPress={handleSend} showsVerticalScrollIndicator={false}
disabled={!message.trim()}
/>
}
/> />
</View> <View style={styles.inputContainer}>
</KeyboardAvoidingView> <LinearGradient
colors={['rgba(26, 26, 26, 0.95)', 'rgba(26, 26, 26, 0.98)']}
style={styles.inputGradient}
/>
<View style={styles.inputWrapper}>
<TextInput
value={message}
onChangeText={setMessage}
placeholder="Введите сообщение..."
placeholderTextColor="#666666"
mode="outlined"
style={styles.input}
multiline
maxLength={1000}
theme={{
colors: {
primary: '#ffffff',
placeholder: '#666666',
text: '#ffffff',
background: 'rgba(255, 255, 255, 0.05)',
outline: '#333333',
}
}}
right={
<TextInput.Icon
icon="send"
onPress={handleSend}
disabled={!message.trim()}
color={message.trim() ? '#ffffff' : '#666666'}
/>
}
/>
</View>
</View>
</KeyboardAvoidingView>
</View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f5f5f5', backgroundColor: '#0a0a0a',
}, },
centerContainer: { keyboardAvoidingView: {
flex: 1,
},
loadingContainer: {
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#0a0a0a',
},
loadingText: {
color: '#666666',
fontSize: 16,
}, },
messagesList: { messagesList: {
padding: 16, padding: 16,
@ -217,47 +290,87 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end', justifyContent: 'flex-end',
}, },
avatar: { avatar: {
marginRight: 8, marginRight: 12,
backgroundColor: '#2d2d2d',
},
avatarLabel: {
color: '#ffffff',
fontSize: 18,
},
messageBubbleContainer: {
maxWidth: '75%',
position: 'relative',
},
ownMessageBubbleContainer: {
alignItems: 'flex-end',
}, },
messageBubble: { messageBubble: {
maxWidth: '75%',
padding: 12, padding: 12,
borderRadius: 16, paddingRight: 16,
backgroundColor: '#fff', borderRadius: 18,
minWidth: 80,
}, },
ownMessageBubble: { otherMessageBubble: {
backgroundColor: '#2196F3', backgroundColor: '#1a1a1a',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
}, },
senderName: { senderName: {
color: '#666', color: '#808080',
marginBottom: 4, marginBottom: 4,
fontSize: 12,
letterSpacing: 0.5,
}, },
messageText: { messageText: {
color: '#000', color: '#ffffff',
fontSize: 15,
lineHeight: 20,
}, },
ownMessageText: { ownMessageText: {
color: '#fff', color: '#000000',
}, },
messageTime: { messageTime: {
color: '#666', color: '#666666',
marginTop: 4, marginTop: 4,
fontSize: 11, fontSize: 11,
}, },
ownMessageTime: { ownMessageTime: {
color: 'rgba(255, 255, 255, 0.7)', color: 'rgba(0, 0, 0, 0.5)',
}, },
menuButton: { menuButton: {
position: 'absolute', position: 'absolute',
top: -8, top: -8,
right: -8, right: -40,
padding: 0,
},
menuContent: {
backgroundColor: '#1a1a1a',
borderRadius: 12,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
menuItemText: {
color: '#ffffff',
}, },
inputContainer: { inputContainer: {
padding: 8, position: 'relative',
backgroundColor: '#fff',
borderTopWidth: 1, borderTopWidth: 1,
borderTopColor: '#e0e0e0', borderTopColor: 'rgba(255, 255, 255, 0.1)',
},
inputGradient: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
},
inputWrapper: {
padding: 12,
}, },
input: { input: {
maxHeight: 100, maxHeight: 100,
fontSize: 16,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 24,
}, },
}); });

View File

@ -1,12 +1,29 @@
<<<<<<< HEAD
import React from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity, Dimensions } from 'react-native';
import { List, Avatar, Text, FAB, Badge, Surface } from 'react-native-paper';
=======
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native'; import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton, useTheme } from 'react-native-paper'; import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton, useTheme } from 'react-native-paper';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import { GET_CONVERSATIONS } from '../graphql/queries'; import { GET_CONVERSATIONS } from '../graphql/queries';
import { Conversation } from '../types'; import { Conversation } from '../types';
import { format } from 'date-fns'; import { format, isToday, isYesterday } from 'date-fns';
import { ru } from 'date-fns/locale'; import { ru } from 'date-fns/locale';
<<<<<<< HEAD
import { LinearGradient } from 'expo-linear-gradient';
import Animated, {
FadeInDown,
FadeInRight,
Layout,
} from 'react-native-reanimated';
const { width } = Dimensions.get('window');
=======
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
export const ConversationsScreen = ({ navigation }: any) => { export const ConversationsScreen = ({ navigation }: any) => {
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@ -14,9 +31,24 @@ export const ConversationsScreen = ({ navigation }: any) => {
const theme = useTheme(); const theme = useTheme();
const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, { const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, {
pollInterval: 5000, // Обновляем каждые 5 секунд pollInterval: 5000,
}); });
<<<<<<< HEAD
const formatMessageTime = (date: string) => {
const messageDate = new Date(date);
if (isToday(messageDate)) {
return format(messageDate, 'HH:mm', { locale: ru });
} else if (isYesterday(messageDate)) {
return 'Вчера';
} else {
return format(messageDate, 'dd.MM', { locale: ru });
}
};
const renderConversation = ({ item, index }: { item: Conversation; index: number }) => {
const otherParticipant = item.participants.find(p => p.id !== data?.me?.id);
=======
// Фильтрация чатов по поисковому запросу // Фильтрация чатов по поисковому запросу
const filteredConversations = data?.conversations?.filter((conv: Conversation) => { const filteredConversations = data?.conversations?.filter((conv: Conversation) => {
if (!searchQuery) return true; if (!searchQuery) return true;
@ -30,31 +62,72 @@ export const ConversationsScreen = ({ navigation }: any) => {
const renderConversation = ({ item }: { item: Conversation }) => { const renderConversation = ({ item }: { item: Conversation }) => {
const otherParticipant = item.participants.find(p => p.id !== user?.id); const otherParticipant = item.participants.find(p => p.id !== user?.id);
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
const displayName = item.isGroup ? item.name : otherParticipant?.username; const displayName = item.isGroup ? item.name : otherParticipant?.username;
const lastMessageTime = item.lastMessage const lastMessageTime = item.lastMessage
? format(new Date(item.lastMessage.createdAt), 'HH:mm', { locale: ru }) ? formatMessageTime(item.lastMessage.createdAt)
: ''; : '';
// Подсчет непрочитанных сообщений (в будущем добавить в GraphQL) // Подсчет непрочитанных сообщений (в будущем добавить в GraphQL)
const unreadCount = 0; const unreadCount = 0;
return ( return (
<TouchableOpacity <Animated.View
onPress={() => navigation.navigate('Chat', { conversationId: item.id, title: displayName })} entering={FadeInDown.delay(index * 50).springify()}
layout={Layout.springify()}
> >
<List.Item <TouchableOpacity
title={displayName || 'Без имени'} onPress={() => navigation.navigate('Chat', { conversationId: item.id, title: displayName })}
description={item.lastMessage?.content || 'Нет сообщений'} activeOpacity={0.7}
left={() => ( >
<View> <Surface style={styles.conversationItem} elevation={0}>
<View style={styles.avatarContainer}>
<LinearGradient
colors={['rgba(255,255,255,0.1)', 'rgba(255,255,255,0.05)']}
style={styles.avatarGradient}
/>
<Avatar.Text <Avatar.Text
size={50} size={52}
label={displayName?.charAt(0).toUpperCase() || '?'} label={displayName?.charAt(0).toUpperCase() || '?'}
style={styles.avatar}
labelStyle={styles.avatarLabel}
theme={{
colors: {
primary: '#2d2d2d',
}
}}
/> />
{otherParticipant?.isOnline && ( {otherParticipant?.isOnline && (
<Badge style={styles.onlineBadge} size={12} /> <Badge style={styles.onlineBadge} size={14} />
)} )}
</View> </View>
<<<<<<< HEAD
<View style={styles.contentContainer}>
<View style={styles.headerRow}>
<Text
variant="titleMedium"
style={styles.conversationTitle}
numberOfLines={1}
>
{displayName || 'Без имени'}
</Text>
<Text variant="bodySmall" style={styles.time}>
{lastMessageTime}
</Text>
</View>
<View style={styles.messageRow}>
<Text
variant="bodyMedium"
style={styles.lastMessage}
numberOfLines={2}
>
{item.lastMessage?.content || 'Нет сообщений'}
</Text>
{/* Здесь можно добавить счетчик непрочитанных */}
</View>
=======
)} )}
right={() => ( right={() => (
<View style={styles.rightContent}> <View style={styles.rightContent}>
@ -64,32 +137,49 @@ export const ConversationsScreen = ({ navigation }: any) => {
{unreadCount > 0 && ( {unreadCount > 0 && (
<Badge style={styles.unreadBadge}>{unreadCount}</Badge> <Badge style={styles.unreadBadge}>{unreadCount}</Badge>
)} )}
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
</View> </View>
)} </Surface>
style={styles.listItem} </TouchableOpacity>
/> </Animated.View>
<Divider />
</TouchableOpacity>
); );
}; };
if (loading && !data) { if (loading && !data) {
return ( return (
<View style={styles.centerContainer}> <View style={styles.loadingContainer}>
<Text>Загрузка...</Text> <LinearGradient
colors={['#0a0a0a', '#1a1a1a']}
style={StyleSheet.absoluteFillObject}
/>
<Text style={styles.loadingText}>Загрузка чатов...</Text>
</View> </View>
); );
} }
if (error) { if (error) {
return ( return (
<View style={styles.centerContainer}> <View style={styles.errorContainer}>
<Text>Ошибка загрузки чатов</Text> <LinearGradient
colors={['#0a0a0a', '#1a1a1a']}
style={StyleSheet.absoluteFillObject}
/>
<Text style={styles.errorText}>Ошибка загрузки чатов</Text>
<TouchableOpacity onPress={() => refetch()} style={styles.retryButton}>
<Text style={styles.retryText}>Попробовать снова</Text>
</TouchableOpacity>
</View> </View>
); );
} }
return ( return (
<<<<<<< HEAD
<View style={styles.container}>
<LinearGradient
colors={['#0a0a0a', '#1a1a1a']}
style={StyleSheet.absoluteFillObject}
/>
=======
<View style={[styles.container, { backgroundColor: theme.colors.background }]}> <View style={[styles.container, { backgroundColor: theme.colors.background }]}>
{/* Поисковая строка */} {/* Поисковая строка */}
<View style={[styles.searchContainer, { backgroundColor: theme.colors.surface }]}> <View style={[styles.searchContainer, { backgroundColor: theme.colors.surface }]}>
@ -102,6 +192,7 @@ export const ConversationsScreen = ({ navigation }: any) => {
/> />
</View> </View>
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
<FlatList <FlatList
data={filteredConversations} data={filteredConversations}
renderItem={renderConversation} renderItem={renderConversation}
@ -109,22 +200,52 @@ export const ConversationsScreen = ({ navigation }: any) => {
onRefresh={refetch} onRefresh={refetch}
refreshing={loading} refreshing={loading}
contentContainerStyle={styles.listContent} contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={ ListEmptyComponent={
<View style={styles.emptyContainer}> <Animated.View
<Text variant="bodyLarge" style={styles.emptyText}> style={styles.emptyContainer}
У вас пока нет чатов entering={FadeInDown.duration(600)}
>
<View style={styles.emptyIconContainer}>
<LinearGradient
colors={['rgba(255,255,255,0.05)', 'rgba(255,255,255,0.02)']}
style={styles.emptyIconGradient}
/>
<Text style={styles.emptyIcon}>💬</Text>
</View>
<Text variant="headlineSmall" style={styles.emptyText}>
Нет активных чатов
</Text> </Text>
<Text variant="bodyMedium" style={styles.emptySubtext}> <Text variant="bodyLarge" style={styles.emptySubtext}>
Начните новый чат, нажав на кнопку внизу Начните новый чат, нажав на кнопку внизу
</Text> </Text>
</View> </Animated.View>
} }
/> />
<<<<<<< HEAD
<Animated.View
entering={FadeInRight.delay(300).springify()}
>
<FAB
icon="plus"
style={styles.fab}
onPress={() => navigation.navigate('NewChat')}
theme={{
colors: {
primaryContainer: '#ffffff',
onPrimaryContainer: '#000000',
}
}}
/>
</Animated.View>
=======
<FAB <FAB
icon="plus" icon="plus"
style={styles.fab} style={styles.fab}
onPress={() => navigation.navigate('Contacts')} onPress={() => navigation.navigate('Contacts')}
/> />
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
</View> </View>
); );
}; };
@ -132,8 +253,11 @@ export const ConversationsScreen = ({ navigation }: any) => {
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#ffffff', backgroundColor: '#0a0a0a',
}, },
<<<<<<< HEAD
loadingContainer: {
=======
searchContainer: { searchContainer: {
padding: 16, padding: 16,
paddingBottom: 8, paddingBottom: 8,
@ -145,20 +269,72 @@ const styles = StyleSheet.create({
backgroundColor: '#f5f5f5', backgroundColor: '#f5f5f5',
}, },
centerContainer: { centerContainer: {
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
backgroundColor: '#0a0a0a',
},
loadingText: {
color: '#666666',
fontSize: 16,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0a0a0a',
},
errorText: {
color: '#ff6b6b',
fontSize: 16,
marginBottom: 20,
},
retryButton: {
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 24,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
retryText: {
color: '#ffffff',
fontSize: 14,
fontWeight: '600',
}, },
listContent: { listContent: {
flexGrow: 1, flexGrow: 1,
},
listItem: {
paddingVertical: 8, paddingVertical: 8,
}, },
rightContent: { conversationItem: {
alignItems: 'flex-end', flexDirection: 'row',
justifyContent: 'center', paddingVertical: 16,
paddingHorizontal: 16,
backgroundColor: 'transparent',
marginHorizontal: 12,
marginVertical: 4,
borderRadius: 16,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.05)',
}, },
<<<<<<< HEAD
avatarContainer: {
position: 'relative',
marginRight: 12,
},
avatarGradient: {
position: 'absolute',
width: 52,
height: 52,
borderRadius: 26,
},
avatar: {
backgroundColor: '#2d2d2d',
},
avatarLabel: {
color: '#ffffff',
fontSize: 20,
fontWeight: '600',
=======
time: { time: {
color: '#666', color: '#666',
fontSize: 12, fontSize: 12,
@ -168,18 +344,68 @@ const styles = StyleSheet.create({
backgroundColor: '#2196F3', backgroundColor: '#2196F3',
color: '#ffffff', color: '#ffffff',
fontSize: 12, fontSize: 12,
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
}, },
onlineBadge: { onlineBadge: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
right: 0, right: 0,
backgroundColor: '#4CAF50', backgroundColor: '#4CAF50',
borderWidth: 2,
borderColor: '#0a0a0a',
},
contentContainer: {
flex: 1,
justifyContent: 'center',
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 4,
},
conversationTitle: {
color: '#ffffff',
fontWeight: '600',
flex: 1,
marginRight: 12,
},
time: {
color: '#666666',
fontSize: 12,
},
messageRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
lastMessage: {
color: '#999999',
flex: 1,
},
separator: {
height: 1,
backgroundColor: 'rgba(255, 255, 255, 0.05)',
marginHorizontal: 28,
marginVertical: 4,
}, },
fab: { fab: {
position: 'absolute', position: 'absolute',
margin: 16, margin: 16,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: '#ffffff',
borderRadius: 16,
// iOS тени
shadowColor: '#ffffff',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 12,
// Android тень
elevation: 8,
}, },
emptyContainer: { emptyContainer: {
flex: 1, flex: 1,
@ -188,13 +414,36 @@ const styles = StyleSheet.create({
paddingHorizontal: 40, paddingHorizontal: 40,
paddingTop: 100, paddingTop: 100,
}, },
emptyIconContainer: {
width: 120,
height: 120,
borderRadius: 60,
justifyContent: 'center',
alignItems: 'center',
marginBottom: 24,
backgroundColor: 'rgba(255, 255, 255, 0.03)',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.05)',
},
emptyIconGradient: {
position: 'absolute',
width: 120,
height: 120,
borderRadius: 60,
},
emptyIcon: {
fontSize: 48,
},
emptyText: { emptyText: {
textAlign: 'center', textAlign: 'center',
marginBottom: 8, marginBottom: 12,
color: '#666', color: '#ffffff',
fontWeight: '300',
letterSpacing: 0.5,
}, },
emptySubtext: { emptySubtext: {
textAlign: 'center', textAlign: 'center',
color: '#999', color: '#666666',
maxWidth: 250,
}, },
}); });

View File

@ -1,9 +1,15 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
<<<<<<< HEAD
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableOpacity } from 'react-native';
import { TextInput, Button, Text, Headline, HelperText, Surface } from 'react-native-paper';
=======
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native'; import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
import { TextInput, Button, Text, Headline, HelperText, useTheme } from 'react-native-paper'; import { TextInput, Button, Text, Headline, HelperText, useTheme } from 'react-native-paper';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { LOGIN } from '../graphql/mutations'; import { LOGIN } from '../graphql/mutations';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { BackgroundDesign } from '../components/BackgroundDesign';
import Animated, { import Animated, {
useSharedValue, useSharedValue,
useAnimatedStyle, useAnimatedStyle,
@ -14,10 +20,13 @@ import Animated, {
withRepeat, withRepeat,
interpolate, interpolate,
Easing, Easing,
FadeIn,
FadeInDown,
FadeInUp,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const AnimatedView = Animated.View;
export const LoginScreen = ({ navigation }: any) => { export const LoginScreen = ({ navigation }: any) => {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -27,25 +36,21 @@ export const LoginScreen = ({ navigation }: any) => {
const theme = useTheme(); const theme = useTheme();
// Анимации // Анимации
const translateY = useSharedValue(50); const cardScale = useSharedValue(0.95);
const opacity = useSharedValue(0); const cardOpacity = useSharedValue(0);
const scale = useSharedValue(0.9);
const glowAnimation = useSharedValue(0); const glowAnimation = useSharedValue(0);
const buttonScale = useSharedValue(1); const buttonScale = useSharedValue(1);
const inputFocusAnimation1 = useSharedValue(0);
const inputFocusAnimation2 = useSharedValue(0);
useEffect(() => { useEffect(() => {
// Анимация появления // Анимация появления карточки
translateY.value = withSpring(0, { damping: 15, stiffness: 100 }); cardScale.value = withSpring(1, { damping: 15, stiffness: 100 });
opacity.value = withTiming(1, { duration: 800 }); cardOpacity.value = withTiming(1, { duration: 800 });
scale.value = withSpring(1, { damping: 15, stiffness: 100 });
// Пульсирующее свечение // Мягкое свечение
glowAnimation.value = withRepeat( glowAnimation.value = withRepeat(
withSequence( withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }), withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) }) withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.ease) })
), ),
-1, -1,
false false
@ -67,24 +72,20 @@ export const LoginScreen = ({ navigation }: any) => {
}; };
// Анимированные стили // Анимированные стили
const containerAnimatedStyle = useAnimatedStyle(() => { const cardAnimatedStyle = useAnimatedStyle(() => {
return { return {
transform: [ transform: [{ scale: cardScale.value }],
{ translateY: translateY.value }, opacity: cardOpacity.value,
{ scale: scale.value }
],
opacity: opacity.value,
}; };
}); });
const glowContainerStyle = useAnimatedStyle(() => { const glowStyle = useAnimatedStyle(() => {
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]); const shadowOpacity = interpolate(glowAnimation.value, [0, 1], [0.1, 0.3]);
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]); const shadowRadius = interpolate(glowAnimation.value, [0, 1], [20, 40]);
return { return {
shadowOpacity: glowOpacity, shadowOpacity: shadowOpacity,
shadowRadius: shadowRadius, shadowRadius: shadowRadius,
elevation: interpolate(glowAnimation.value, [0, 1], [5, 15]),
}; };
}); });
@ -94,21 +95,7 @@ export const LoginScreen = ({ navigation }: any) => {
}; };
}); });
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 = () => { const handleButtonPressIn = () => {
buttonScale.value = withSpring(0.95); buttonScale.value = withSpring(0.95);
@ -119,6 +106,17 @@ export const LoginScreen = ({ navigation }: any) => {
}; };
return ( return (
<<<<<<< HEAD
<BackgroundDesign variant="login">
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
>
<ScrollView contentContainerStyle={styles.scrollContent}>
<Animated.View
style={[styles.content, cardAnimatedStyle]}
entering={FadeInDown.duration(800).springify()}
=======
<KeyboardAvoidingView <KeyboardAvoidingView
style={[styles.container, { backgroundColor: theme.colors.background }]} style={[styles.container, { backgroundColor: theme.colors.background }]}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
@ -229,19 +227,142 @@ export const LoginScreen = ({ navigation }: any) => {
primary: '#a855f7', primary: '#a855f7',
} }
}} }}
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
> >
Нет аккаунта? Зарегистрироваться <Animated.View style={[styles.loginCard, glowStyle]}>
</Button> <LinearGradient
</Animated.View> colors={['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)']}
</ScrollView> style={styles.gradientBackground}
</KeyboardAvoidingView> start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
<Animated.View
entering={FadeInUp.delay(200).duration(600)}
style={styles.headerContainer}
>
<View style={styles.logoContainer}>
<View style={styles.logoPlaceholder}>
<Text style={styles.logoText}>P</Text>
</View>
</View>
<Headline style={styles.title}>PRISM</Headline>
</Animated.View>
<Animated.View
entering={FadeIn.delay(400).duration(600)}
style={styles.formContainer}
>
<TextInput
label="Имя пользователя"
value={username}
onChangeText={setUsername}
mode="flat"
style={styles.input}
autoCapitalize="none"
disabled={loading}
underlineColor="transparent"
activeUnderlineColor="#ffffff"
theme={{
colors: {
primary: '#ffffff',
placeholder: '#808080',
text: '#ffffff',
background: 'rgba(255,255,255,0.05)',
outline: '#666666',
}
}}
/>
<TextInput
label="Пароль"
value={password}
onChangeText={setPassword}
mode="flat"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
underlineColor="transparent"
activeUnderlineColor="#ffffff"
theme={{
colors: {
primary: '#ffffff',
placeholder: '#808080',
text: '#ffffff',
background: 'rgba(255,255,255,0.05)',
outline: '#666666',
}
}}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
color="#808080"
/>
}
/>
{error && (
<HelperText type="error" visible={true} style={styles.errorText}>
{error.message}
</HelperText>
)}
<Animated.View style={buttonAnimatedStyle}>
<TouchableOpacity
onPress={handleLogin}
onPressIn={handleButtonPressIn}
onPressOut={handleButtonPressOut}
disabled={loading || !username || !password}
activeOpacity={0.8}
>
<LinearGradient
colors={['#ffffff', '#e6e6e6']}
style={[
styles.gradientButton,
(loading || !username || !password) && styles.disabledButton
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.buttonText}>
{loading ? 'ВХОД...' : 'ВОЙТИ'}
</Text>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
<View style={styles.dividerContainer}>
<View style={styles.divider} />
<Text style={styles.dividerText}>ИЛИ</Text>
<View style={styles.divider} />
</View>
<TouchableOpacity
onPress={() => navigation.navigate('Register')}
disabled={loading}
style={styles.linkButton}
activeOpacity={0.7}
>
<Text style={styles.linkButtonText}>
Нет аккаунта?
</Text>
<Text style={styles.linkButtonTextBold}>
{' Создать'}
</Text>
</TouchableOpacity>
</Animated.View>
</Animated.View>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
</BackgroundDesign>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#0a0a0f',
}, },
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
@ -254,90 +375,135 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
alignSelf: 'center', alignSelf: 'center',
}, },
glowContainer: { loginCard: {
marginBottom: 40, borderRadius: 24,
padding: 20, padding: 32,
borderRadius: 20, backgroundColor: 'rgba(26, 26, 26, 0.8)',
backgroundColor: '#1a1a2e', borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
// backdropFilter: 'blur(10px)', // не поддерживается в React Native
// iOS тени // iOS тени
shadowColor: '#9333ea', shadowColor: '#ffffff',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 0, height: 0,
}, },
shadowOpacity: 0.5, shadowOpacity: 0.1,
shadowRadius: 20, shadowRadius: 30,
// Android тень // Android тень
elevation: 10, elevation: 20,
}, },
title: { gradientBackground: {
textAlign: 'center', position: 'absolute',
marginBottom: 10, left: 0,
fontSize: 32, right: 0,
top: 0,
bottom: 0,
borderRadius: 24,
},
headerContainer: {
alignItems: 'center',
marginBottom: 40,
},
logoContainer: {
marginBottom: 20,
},
logoPlaceholder: {
width: 80,
height: 80,
borderRadius: 20,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: 'rgba(255, 255, 255, 0.2)',
},
logoText: {
fontSize: 40,
fontWeight: 'bold', fontWeight: 'bold',
color: '#ffffff', color: '#ffffff',
textShadowColor: '#9333ea',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 10,
}, },
subtitle: { title: {
textAlign: 'center', fontSize: 36,
fontSize: 16, fontWeight: '300',
color: '#a855f7', color: '#ffffff',
fontStyle: 'italic', letterSpacing: 4,
marginBottom: 8,
}, },
formContainer: {
width: '100%',
},
input: { input: {
marginBottom: 20, marginBottom: 20,
backgroundColor: '#1a1a2e', backgroundColor: 'rgba(255, 255, 255, 0.05)',
fontSize: 16,
borderRadius: 12, borderRadius: 12,
paddingHorizontal: 16,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
errorText: {
color: '#ff6b6b',
textAlign: 'center',
marginBottom: 16,
fontSize: 14,
},
gradientButton: {
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 12,
alignItems: 'center',
marginTop: 8,
// iOS тени // iOS тени
shadowColor: '#7c3aed', shadowColor: '#ffffff',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 4, height: 4,
}, },
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 6, shadowRadius: 12,
// Android тень // Android тень
elevation: 5, elevation: 8,
}, },
button: { disabledButton: {
marginTop: 20, opacity: 0.5,
marginBottom: 10,
borderRadius: 12,
backgroundColor: '#9333ea',
// iOS тени для кнопки
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.6,
shadowRadius: 15,
// Android тень
elevation: 10,
}, },
buttonContent: { buttonText: {
paddingVertical: 8, color: '#000000',
fontSize: 16,
fontWeight: '700',
letterSpacing: 2,
}, },
buttonLabel: { dividerContainer: {
fontSize: 18, flexDirection: 'row',
fontWeight: 'bold', alignItems: 'center',
marginVertical: 24,
},
divider: {
flex: 1,
height: 1,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
dividerText: {
color: '#666666',
paddingHorizontal: 16,
fontSize: 12,
letterSpacing: 1, letterSpacing: 1,
}, },
linkButton: { linkButton: {
marginTop: 10, flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 12,
}, },
linkButtonLabel: { linkButtonText: {
fontSize: 16, fontSize: 14,
color: '#a855f7', color: '#808080',
}, },
errorText: { linkButtonTextBold: {
color: '#ef4444', fontSize: 14,
textAlign: 'center', color: '#ffffff',
marginBottom: 10, fontWeight: '600',
textShadowColor: '#ef4444',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 5,
}, },
}); });

View File

@ -1,9 +1,10 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native'; import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableOpacity } from 'react-native';
import { TextInput, Button, Text, Headline, HelperText } from 'react-native-paper'; import { TextInput, Button, Text, Headline, HelperText, Surface } from 'react-native-paper';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import { REGISTER } from '../graphql/mutations'; import { REGISTER } from '../graphql/mutations';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import { BackgroundDesign } from '../components/BackgroundDesign';
import Animated, { import Animated, {
useSharedValue, useSharedValue,
useAnimatedStyle, useAnimatedStyle,
@ -14,10 +15,13 @@ import Animated, {
withRepeat, withRepeat,
interpolate, interpolate,
Easing, Easing,
FadeIn,
FadeInDown,
FadeInUp,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
const { width: screenWidth } = Dimensions.get('window'); const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const AnimatedView = Animated.View;
export const RegisterScreen = ({ navigation }: any) => { export const RegisterScreen = ({ navigation }: any) => {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('');
@ -28,27 +32,21 @@ export const RegisterScreen = ({ navigation }: any) => {
const { login } = useAuth(); const { login } = useAuth();
// Анимации // Анимации
const translateY = useSharedValue(50); const cardScale = useSharedValue(0.95);
const opacity = useSharedValue(0); const cardOpacity = useSharedValue(0);
const scale = useSharedValue(0.9);
const glowAnimation = useSharedValue(0); const glowAnimation = useSharedValue(0);
const buttonScale = useSharedValue(1); const buttonScale = useSharedValue(1);
const inputFocusAnimation1 = useSharedValue(0);
const inputFocusAnimation2 = useSharedValue(0);
const inputFocusAnimation3 = useSharedValue(0);
const inputFocusAnimation4 = useSharedValue(0);
useEffect(() => { useEffect(() => {
// Анимация появления // Анимация появления карточки
translateY.value = withSpring(0, { damping: 15, stiffness: 100 }); cardScale.value = withSpring(1, { damping: 15, stiffness: 100 });
opacity.value = withTiming(1, { duration: 800 }); cardOpacity.value = withTiming(1, { duration: 800 });
scale.value = withSpring(1, { damping: 15, stiffness: 100 });
// Пульсирующее свечение // Мягкое свечение
glowAnimation.value = withRepeat( glowAnimation.value = withRepeat(
withSequence( withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }), withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) }) withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.ease) })
), ),
-1, -1,
false false
@ -72,24 +70,20 @@ export const RegisterScreen = ({ navigation }: any) => {
const passwordsMatch = password === confirmPassword || confirmPassword === ''; const passwordsMatch = password === confirmPassword || confirmPassword === '';
// Анимированные стили // Анимированные стили
const containerAnimatedStyle = useAnimatedStyle(() => { const cardAnimatedStyle = useAnimatedStyle(() => {
return { return {
transform: [ transform: [{ scale: cardScale.value }],
{ translateY: translateY.value }, opacity: cardOpacity.value,
{ scale: scale.value }
],
opacity: opacity.value,
}; };
}); });
const glowContainerStyle = useAnimatedStyle(() => { const glowStyle = useAnimatedStyle(() => {
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]); const shadowOpacity = interpolate(glowAnimation.value, [0, 1], [0.1, 0.3]);
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]); const shadowRadius = interpolate(glowAnimation.value, [0, 1], [20, 40]);
return { return {
shadowOpacity: glowOpacity, shadowOpacity: shadowOpacity,
shadowRadius: shadowRadius, shadowRadius: shadowRadius,
elevation: interpolate(glowAnimation.value, [0, 1], [5, 15]),
}; };
}); });
@ -99,23 +93,7 @@ export const RegisterScreen = ({ navigation }: any) => {
}; };
}); });
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 = () => { const handleButtonPressIn = () => {
buttonScale.value = withSpring(0.95); buttonScale.value = withSpring(0.95);
@ -126,192 +104,196 @@ export const RegisterScreen = ({ navigation }: any) => {
}; };
return ( return (
<KeyboardAvoidingView <BackgroundDesign variant="login">
style={styles.container} <KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}
> behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
<ScrollView contentContainerStyle={styles.scrollContent}> >
<Animated.View style={[styles.content, containerAnimatedStyle]}> <ScrollView contentContainerStyle={styles.scrollContent}>
<Animated.View style={[styles.glowContainer, glowContainerStyle]}> <Animated.View
<Headline style={styles.title}>Регистрация в Prism</Headline> style={[styles.content, cardAnimatedStyle]}
<Text style={styles.subtitle}>Присоединяйтесь к будущему</Text> entering={FadeInDown.duration(800).springify()}
</Animated.View>
<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>
<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>
<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>
<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} style={styles.errorText}>
Пароли не совпадают
</HelperText>
)}
{error && (
<HelperText type="error" visible={true} style={styles.errorText}>
{error.message}
</HelperText>
)}
<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',
}
}}
> >
Уже есть аккаунт? Войти <Animated.View style={[styles.registerCard, glowStyle]}>
</Button> <LinearGradient
</Animated.View> colors={['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)']}
</ScrollView> style={styles.gradientBackground}
</KeyboardAvoidingView> start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
<Animated.View
entering={FadeInUp.delay(200).duration(600)}
style={styles.headerContainer}
>
<Headline style={styles.title}>РЕГИСТРАЦИЯ</Headline>
</Animated.View>
<Animated.View
entering={FadeIn.delay(400).duration(600)}
style={styles.formContainer}
>
<TextInput
label="Имя пользователя"
value={username}
onChangeText={setUsername}
mode="flat"
style={styles.input}
autoCapitalize="none"
disabled={loading}
theme={{
colors: {
primary: '#ffffff',
placeholder: '#808080',
text: '#ffffff',
background: 'rgba(255,255,255,0.05)',
outline: '#666666',
}
}}
underlineColor="transparent"
activeUnderlineColor="#ffffff"
/>
<TextInput
label="Электронная почта"
value={email}
onChangeText={setEmail}
mode="flat"
style={styles.input}
keyboardType="email-address"
autoCapitalize="none"
disabled={loading}
theme={{
colors: {
primary: '#ffffff',
placeholder: '#808080',
text: '#ffffff',
background: 'rgba(255,255,255,0.05)',
outline: '#666666',
}
}}
underlineColor="transparent"
activeUnderlineColor="#ffffff"
/>
<TextInput
label="Пароль"
value={password}
onChangeText={setPassword}
mode="flat"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
theme={{
colors: {
primary: '#ffffff',
placeholder: '#808080',
text: '#ffffff',
background: 'rgba(255,255,255,0.05)',
outline: '#666666',
}
}}
right={
<TextInput.Icon
icon={showPassword ? 'eye-off' : 'eye'}
onPress={() => setShowPassword(!showPassword)}
color="#808080"
/>
}
underlineColor="transparent"
activeUnderlineColor="#ffffff"
/>
<TextInput
label="Подтвердите пароль"
value={confirmPassword}
onChangeText={setConfirmPassword}
mode="flat"
style={styles.input}
secureTextEntry={!showPassword}
disabled={loading}
error={!passwordsMatch}
theme={{
colors: {
primary: '#ffffff',
placeholder: '#808080',
text: '#ffffff',
background: 'rgba(255,255,255,0.05)',
outline: passwordsMatch ? '#666666' : '#ff6b6b',
error: '#ff6b6b',
}
}}
underlineColor="transparent"
activeUnderlineColor="#ffffff"
/>
{!passwordsMatch && confirmPassword !== '' && (
<HelperText type="error" visible={true} style={styles.errorText}>
Пароли не совпадают
</HelperText>
)}
{error && (
<HelperText type="error" visible={true} style={styles.errorText}>
{error.message}
</HelperText>
)}
<Animated.View style={buttonAnimatedStyle}>
<TouchableOpacity
onPress={handleRegister}
onPressIn={handleButtonPressIn}
onPressOut={handleButtonPressOut}
disabled={loading || !username || !email || !password || !passwordsMatch}
activeOpacity={0.8}
>
<LinearGradient
colors={['#ffffff', '#e6e6e6']}
style={[
styles.gradientButton,
(loading || !username || !email || !password || !passwordsMatch) && styles.disabledButton
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.buttonText}>
{loading ? 'СОЗДАНИЕ...' : 'СОЗДАТЬ АККАУНТ'}
</Text>
</LinearGradient>
</TouchableOpacity>
</Animated.View>
<View style={styles.dividerContainer}>
<View style={styles.divider} />
<Text style={styles.dividerText}>ИЛИ</Text>
<View style={styles.divider} />
</View>
<TouchableOpacity
onPress={() => navigation.navigate('Login')}
disabled={loading}
style={styles.linkButton}
activeOpacity={0.7}
>
<Text style={styles.linkButtonText}>
Уже есть аккаунт?
</Text>
<Text style={styles.linkButtonTextBold}>
{' Войти'}
</Text>
</TouchableOpacity>
</Animated.View>
</Animated.View>
</Animated.View>
</ScrollView>
</KeyboardAvoidingView>
</BackgroundDesign>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#0a0a0f',
}, },
scrollContent: { scrollContent: {
flexGrow: 1, flexGrow: 1,
@ -324,90 +306,117 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
alignSelf: 'center', alignSelf: 'center',
}, },
glowContainer: { registerCard: {
marginBottom: 40, borderRadius: 24,
padding: 20, padding: 32,
borderRadius: 20, backgroundColor: 'rgba(26, 26, 26, 0.8)',
backgroundColor: '#1a1a2e', borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
// backdropFilter: 'blur(10px)', // не поддерживается в React Native
// iOS тени // iOS тени
shadowColor: '#9333ea', shadowColor: '#ffffff',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 0, height: 0,
}, },
shadowOpacity: 0.5, shadowOpacity: 0.1,
shadowRadius: 20, shadowRadius: 30,
// Android тень // Android тень
elevation: 10, elevation: 20,
},
gradientBackground: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
borderRadius: 24,
},
headerContainer: {
alignItems: 'center',
marginBottom: 40,
}, },
title: { title: {
textAlign: 'center', fontSize: 28,
marginBottom: 10, fontWeight: '300',
fontSize: 32,
fontWeight: 'bold',
color: '#ffffff', color: '#ffffff',
textShadowColor: '#9333ea', letterSpacing: 3,
textShadowOffset: { width: 0, height: 0 }, marginBottom: 8,
textShadowRadius: 10,
}, },
subtitle: {
textAlign: 'center', formContainer: {
fontSize: 16, width: '100%',
color: '#a855f7',
fontStyle: 'italic',
}, },
input: { input: {
marginBottom: 20, marginBottom: 20,
backgroundColor: '#1a1a2e', backgroundColor: 'rgba(255, 255, 255, 0.05)',
fontSize: 16,
borderRadius: 12, borderRadius: 12,
paddingHorizontal: 16,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
errorText: {
color: '#ff6b6b',
textAlign: 'center',
marginBottom: 16,
fontSize: 14,
},
gradientButton: {
paddingVertical: 16,
paddingHorizontal: 32,
borderRadius: 12,
alignItems: 'center',
marginTop: 8,
// iOS тени // iOS тени
shadowColor: '#7c3aed', shadowColor: '#ffffff',
shadowOffset: { shadowOffset: {
width: 0, width: 0,
height: 4, height: 4,
}, },
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 6, shadowRadius: 12,
// Android тень // Android тень
elevation: 5, elevation: 8,
}, },
button: { disabledButton: {
marginTop: 20, opacity: 0.5,
marginBottom: 10,
borderRadius: 12,
backgroundColor: '#9333ea',
// iOS тени для кнопки
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 8,
},
shadowOpacity: 0.6,
shadowRadius: 15,
// Android тень
elevation: 10,
}, },
buttonContent: { buttonText: {
paddingVertical: 8, color: '#000000',
fontSize: 16,
fontWeight: '700',
letterSpacing: 2,
}, },
buttonLabel: { dividerContainer: {
fontSize: 18, flexDirection: 'row',
fontWeight: 'bold', alignItems: 'center',
marginVertical: 24,
},
divider: {
flex: 1,
height: 1,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
dividerText: {
color: '#666666',
paddingHorizontal: 16,
fontSize: 12,
letterSpacing: 1, letterSpacing: 1,
}, },
linkButton: { linkButton: {
marginTop: 10, flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 12,
}, },
linkButtonLabel: { linkButtonText: {
fontSize: 16, fontSize: 14,
color: '#a855f7', color: '#808080',
}, },
errorText: { linkButtonTextBold: {
color: '#ef4444', fontSize: 14,
textAlign: 'center', color: '#ffffff',
marginBottom: 10, fontWeight: '600',
textShadowColor: '#ef4444',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 5,
}, },
}); });