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 { StatusBar } from 'expo-status-bar';
import { Platform } from 'react-native';
import { ApolloProvider } from '@apollo/client';
import { Provider as PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { apolloClient } from './src/services/apollo-client';
import { AuthProvider } from './src/contexts/AuthContext';
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';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
export default function App() {
return (

View File

@ -16,6 +16,7 @@
"@react-navigation/native-stack": "^7.3.24",
"date-fns": "^4.1.0",
"expo": "~53.0.20",
"expo-linear-gradient": "^14.1.5",
"expo-status-bar": "~2.2.3",
"graphql": "^16.11.0",
"react": "19.0.0",
@ -27,6 +28,7 @@
"react-native-reanimated": "~3.17.4",
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-svg": "15.11.2",
"react-native-vector-icons": "^10.3.0",
"react-native-web": "^0.20.0"
},
@ -3425,6 +3427,12 @@
"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": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@ -3997,6 +4005,56 @@
"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": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -4110,6 +4168,61 @@
"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": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@ -4170,6 +4283,18 @@
"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": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@ -4371,6 +4496,17 @@
"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": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz",
@ -5733,6 +5869,12 @@
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
"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": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -6343,6 +6485,18 @@
"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": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@ -7221,6 +7375,21 @@
"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": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",

View File

@ -10,15 +10,18 @@
},
"dependencies": {
"@apollo/client": "^3.13.9",
"@expo/metro-runtime": "~5.0.4",
"@react-native-async-storage/async-storage": "2.1.2",
"@react-navigation/bottom-tabs": "^7.4.5",
"@react-navigation/native": "^7.1.17",
"@react-navigation/native-stack": "^7.3.24",
"date-fns": "^4.1.0",
"expo": "~53.0.20",
"expo-linear-gradient": "^14.1.5",
"expo-status-bar": "~2.2.3",
"graphql": "^16.11.0",
"react": "19.0.0",
"react-dom": "19.0.0",
"react-native": "0.79.5",
"react-native-gesture-handler": "~2.24.0",
"react-native-keyboard-aware-scroll-view": "^0.9.5",
@ -27,9 +30,8 @@
"react-native-safe-area-context": "5.4.0",
"react-native-screens": "~4.11.1",
"react-native-vector-icons": "^10.3.0",
"react-dom": "19.0.0",
"react-native-web": "^0.20.0",
"@expo/metro-runtime": "~5.0.4"
"react-native-svg": "15.11.2"
},
"devDependencies": {
"@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 { 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 { useQuery, useMutation, useSubscription } from '@apollo/client';
import { GET_MESSAGES } from '../graphql/queries';
@ -9,6 +9,14 @@ import { Message } from '../types';
import { useAuth } from '../contexts/AuthContext';
import { format } from 'date-fns';
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) => {
const { conversationId, title } = route.params;
@ -61,7 +69,6 @@ export const ChatScreen = ({ route }: any) => {
variables: { conversationId },
onData: ({ data }) => {
if (data?.data?.messageAdded && data.data.messageAdded.sender.id !== user?.id) {
// Сообщение от другого пользователя
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 messageTime = format(new Date(item.createdAt), 'HH:mm', { locale: ru });
return (
<View style={[styles.messageContainer, isOwnMessage && styles.ownMessageContainer]}>
{!isOwnMessage && (
<Avatar.Text
size={36}
label={item.sender.username.charAt(0).toUpperCase()}
style={styles.avatar}
/>
)}
<Surface
style={[styles.messageBubble, isOwnMessage && styles.ownMessageBubble]}
elevation={1}
<Animated.View
entering={isOwnMessage ? SlideInRight.delay(index * 50) : SlideInLeft.delay(index * 50)}
exiting={FadeOutDown}
layout={Layout.springify()}
style={[styles.messageContainer, isOwnMessage && styles.ownMessageContainer]}
>
{!isOwnMessage && (
<Text variant="labelSmall" style={styles.senderName}>
{item.sender.username}
</Text>
<Avatar.Text
size={40}
label={item.sender.username.charAt(0).toUpperCase()}
style={styles.avatar}
labelStyle={styles.avatarLabel}
theme={{
colors: {
primary: '#2d2d2d',
}
}}
/>
)}
<Text style={[styles.messageText, isOwnMessage && styles.ownMessageText]}>
<View style={[styles.messageBubbleContainer, isOwnMessage && styles.ownMessageBubbleContainer]}>
{isOwnMessage ? (
<LinearGradient
colors={['#ffffff', '#f0f0f0']}
style={styles.messageBubble}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
>
<Text style={[styles.messageText, styles.ownMessageText]}>
{item.content}
</Text>
<Text variant="bodySmall" style={[styles.messageTime, isOwnMessage && styles.ownMessageTime]}>
<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>
)}
{isOwnMessage && (
<Menu
visible={menuVisible && selectedMessage === item.id}
@ -131,36 +163,50 @@ export const ChatScreen = ({ route }: any) => {
setSelectedMessage(null);
}}
anchor={
<IconButton
icon="dots-vertical"
size={16}
<TouchableOpacity
onPress={() => {
setSelectedMessage(item.id);
setMenuVisible(true);
}}
style={styles.menuButton}
/>
}
>
<Menu.Item onPress={() => handleDeleteMessage(item.id)} title="Удалить" />
<IconButton
icon="dots-vertical"
size={20}
iconColor="#666666"
/>
</TouchableOpacity>
}
contentStyle={styles.menuContent}
>
<Menu.Item
onPress={() => handleDeleteMessage(item.id)}
title="Удалить"
titleStyle={styles.menuItemText}
/>
</Menu>
)}
</Surface>
</View>
</Animated.View>
);
};
if (loading && !data) {
return (
<View style={styles.centerContainer}>
<Text>Загрузка сообщений...</Text>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>Загрузка сообщений...</Text>
</View>
);
}
return (
<View style={styles.container}>
<LinearGradient
colors={['#0a0a0a', '#1a1a1a']}
style={StyleSheet.absoluteFillObject}
/>
<KeyboardAvoidingView
style={styles.container}
style={styles.keyboardAvoidingView}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
keyboardVerticalOffset={90}
>
@ -171,38 +217,65 @@ export const ChatScreen = ({ route }: any) => {
keyExtractor={(item) => item.id}
contentContainerStyle={styles.messagesList}
onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
showsVerticalScrollIndicator={false}
/>
<View style={styles.inputContainer}>
<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({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
backgroundColor: '#0a0a0a',
},
centerContainer: {
keyboardAvoidingView: {
flex: 1,
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#0a0a0a',
},
loadingText: {
color: '#666666',
fontSize: 16,
},
messagesList: {
padding: 16,
@ -217,47 +290,87 @@ const styles = StyleSheet.create({
justifyContent: 'flex-end',
},
avatar: {
marginRight: 8,
marginRight: 12,
backgroundColor: '#2d2d2d',
},
avatarLabel: {
color: '#ffffff',
fontSize: 18,
},
messageBubbleContainer: {
maxWidth: '75%',
position: 'relative',
},
ownMessageBubbleContainer: {
alignItems: 'flex-end',
},
messageBubble: {
maxWidth: '75%',
padding: 12,
borderRadius: 16,
backgroundColor: '#fff',
paddingRight: 16,
borderRadius: 18,
minWidth: 80,
},
ownMessageBubble: {
backgroundColor: '#2196F3',
otherMessageBubble: {
backgroundColor: '#1a1a1a',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
senderName: {
color: '#666',
color: '#808080',
marginBottom: 4,
fontSize: 12,
letterSpacing: 0.5,
},
messageText: {
color: '#000',
color: '#ffffff',
fontSize: 15,
lineHeight: 20,
},
ownMessageText: {
color: '#fff',
color: '#000000',
},
messageTime: {
color: '#666',
color: '#666666',
marginTop: 4,
fontSize: 11,
},
ownMessageTime: {
color: 'rgba(255, 255, 255, 0.7)',
color: 'rgba(0, 0, 0, 0.5)',
},
menuButton: {
position: 'absolute',
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: {
padding: 8,
backgroundColor: '#fff',
position: 'relative',
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: {
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 { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton, useTheme } from 'react-native-paper';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
import { useQuery } from '@apollo/client';
import { GET_CONVERSATIONS } from '../graphql/queries';
import { Conversation } from '../types';
import { format } from 'date-fns';
import { format, isToday, isYesterday } from 'date-fns';
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';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
export const ConversationsScreen = ({ navigation }: any) => {
const [searchQuery, setSearchQuery] = useState('');
@ -14,9 +31,24 @@ export const ConversationsScreen = ({ navigation }: any) => {
const theme = useTheme();
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) => {
if (!searchQuery) return true;
@ -30,31 +62,72 @@ export const ConversationsScreen = ({ navigation }: any) => {
const renderConversation = ({ item }: { item: Conversation }) => {
const otherParticipant = item.participants.find(p => p.id !== user?.id);
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
const displayName = item.isGroup ? item.name : otherParticipant?.username;
const lastMessageTime = item.lastMessage
? format(new Date(item.lastMessage.createdAt), 'HH:mm', { locale: ru })
? formatMessageTime(item.lastMessage.createdAt)
: '';
// Подсчет непрочитанных сообщений (в будущем добавить в GraphQL)
const unreadCount = 0;
return (
<Animated.View
entering={FadeInDown.delay(index * 50).springify()}
layout={Layout.springify()}
>
<TouchableOpacity
onPress={() => navigation.navigate('Chat', { conversationId: item.id, title: displayName })}
activeOpacity={0.7}
>
<List.Item
title={displayName || 'Без имени'}
description={item.lastMessage?.content || 'Нет сообщений'}
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
size={50}
size={52}
label={displayName?.charAt(0).toUpperCase() || '?'}
style={styles.avatar}
labelStyle={styles.avatarLabel}
theme={{
colors: {
primary: '#2d2d2d',
}
}}
/>
{otherParticipant?.isOnline && (
<Badge style={styles.onlineBadge} size={12} />
<Badge style={styles.onlineBadge} size={14} />
)}
</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={() => (
<View style={styles.rightContent}>
@ -64,32 +137,49 @@ export const ConversationsScreen = ({ navigation }: any) => {
{unreadCount > 0 && (
<Badge style={styles.unreadBadge}>{unreadCount}</Badge>
)}
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
</View>
)}
style={styles.listItem}
/>
<Divider />
</Surface>
</TouchableOpacity>
</Animated.View>
);
};
if (loading && !data) {
return (
<View style={styles.centerContainer}>
<Text>Загрузка...</Text>
<View style={styles.loadingContainer}>
<LinearGradient
colors={['#0a0a0a', '#1a1a1a']}
style={StyleSheet.absoluteFillObject}
/>
<Text style={styles.loadingText}>Загрузка чатов...</Text>
</View>
);
}
if (error) {
return (
<View style={styles.centerContainer}>
<Text>Ошибка загрузки чатов</Text>
<View style={styles.errorContainer}>
<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>
);
}
return (
<<<<<<< HEAD
<View style={styles.container}>
<LinearGradient
colors={['#0a0a0a', '#1a1a1a']}
style={StyleSheet.absoluteFillObject}
/>
=======
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
{/* Поисковая строка */}
<View style={[styles.searchContainer, { backgroundColor: theme.colors.surface }]}>
@ -102,6 +192,7 @@ export const ConversationsScreen = ({ navigation }: any) => {
/>
</View>
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
<FlatList
data={filteredConversations}
renderItem={renderConversation}
@ -109,22 +200,52 @@ export const ConversationsScreen = ({ navigation }: any) => {
onRefresh={refetch}
refreshing={loading}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={styles.separator} />}
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text variant="bodyLarge" style={styles.emptyText}>
У вас пока нет чатов
<Animated.View
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 variant="bodyMedium" style={styles.emptySubtext}>
<Text variant="bodyLarge" style={styles.emptySubtext}>
Начните новый чат, нажав на кнопку внизу
</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
icon="plus"
style={styles.fab}
onPress={() => navigation.navigate('Contacts')}
/>
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
</View>
);
};
@ -132,8 +253,11 @@ export const ConversationsScreen = ({ navigation }: any) => {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#ffffff',
backgroundColor: '#0a0a0a',
},
<<<<<<< HEAD
loadingContainer: {
=======
searchContainer: {
padding: 16,
paddingBottom: 8,
@ -145,20 +269,72 @@ const styles = StyleSheet.create({
backgroundColor: '#f5f5f5',
},
centerContainer: {
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
flex: 1,
justifyContent: '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: {
flexGrow: 1,
},
listItem: {
paddingVertical: 8,
},
rightContent: {
alignItems: 'flex-end',
justifyContent: 'center',
conversationItem: {
flexDirection: 'row',
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: {
color: '#666',
fontSize: 12,
@ -168,18 +344,68 @@ const styles = StyleSheet.create({
backgroundColor: '#2196F3',
color: '#ffffff',
fontSize: 12,
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
},
onlineBadge: {
position: 'absolute',
bottom: 0,
right: 0,
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: {
position: 'absolute',
margin: 16,
right: 0,
bottom: 0,
backgroundColor: '#ffffff',
borderRadius: 16,
// iOS тени
shadowColor: '#ffffff',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 12,
// Android тень
elevation: 8,
},
emptyContainer: {
flex: 1,
@ -188,13 +414,36 @@ const styles = StyleSheet.create({
paddingHorizontal: 40,
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: {
textAlign: 'center',
marginBottom: 8,
color: '#666',
marginBottom: 12,
color: '#ffffff',
fontWeight: '300',
letterSpacing: 0.5,
},
emptySubtext: {
textAlign: 'center',
color: '#999',
color: '#666666',
maxWidth: 250,
},
});

View File

@ -1,9 +1,15 @@
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 { TextInput, Button, Text, Headline, HelperText, useTheme } from 'react-native-paper';
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
import { useMutation } from '@apollo/client';
import { LOGIN } from '../graphql/mutations';
import { useAuth } from '../contexts/AuthContext';
import { BackgroundDesign } from '../components/BackgroundDesign';
import Animated, {
useSharedValue,
useAnimatedStyle,
@ -14,10 +20,13 @@ import Animated, {
withRepeat,
interpolate,
Easing,
FadeIn,
FadeInDown,
FadeInUp,
} from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
const { width: screenWidth } = Dimensions.get('window');
const AnimatedView = Animated.View;
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
export const LoginScreen = ({ navigation }: any) => {
const [username, setUsername] = useState('');
@ -27,25 +36,21 @@ export const LoginScreen = ({ navigation }: any) => {
const theme = useTheme();
// Анимации
const translateY = useSharedValue(50);
const opacity = useSharedValue(0);
const scale = useSharedValue(0.9);
const cardScale = useSharedValue(0.95);
const cardOpacity = useSharedValue(0);
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 });
// Анимация появления карточки
cardScale.value = withSpring(1, { damping: 15, stiffness: 100 });
cardOpacity.value = withTiming(1, { duration: 800 });
// Пульсирующее свечение
// Мягкое свечение
glowAnimation.value = withRepeat(
withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) })
withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.ease) }),
withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.ease) })
),
-1,
false
@ -67,24 +72,20 @@ export const LoginScreen = ({ navigation }: any) => {
};
// Анимированные стили
const containerAnimatedStyle = useAnimatedStyle(() => {
const cardAnimatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateY: translateY.value },
{ scale: scale.value }
],
opacity: opacity.value,
transform: [{ scale: cardScale.value }],
opacity: cardOpacity.value,
};
});
const glowContainerStyle = useAnimatedStyle(() => {
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]);
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]);
const glowStyle = useAnimatedStyle(() => {
const shadowOpacity = interpolate(glowAnimation.value, [0, 1], [0.1, 0.3]);
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [20, 40]);
return {
shadowOpacity: glowOpacity,
shadowOpacity: shadowOpacity,
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 = () => {
buttonScale.value = withSpring(0.95);
@ -119,6 +106,17 @@ export const LoginScreen = ({ navigation }: any) => {
};
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
style={[styles.container, { backgroundColor: theme.colors.background }]}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
@ -229,19 +227,142 @@ export const LoginScreen = ({ navigation }: any) => {
primary: '#a855f7',
}
}}
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
>
Нет аккаунта? Зарегистрироваться
</Button>
<Animated.View style={[styles.loginCard, glowStyle]}>
<LinearGradient
colors={['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)']}
style={styles.gradientBackground}
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({
container: {
flex: 1,
backgroundColor: '#0a0a0f',
},
scrollContent: {
flexGrow: 1,
@ -254,90 +375,135 @@ const styles = StyleSheet.create({
width: '100%',
alignSelf: 'center',
},
glowContainer: {
marginBottom: 40,
padding: 20,
borderRadius: 20,
backgroundColor: '#1a1a2e',
loginCard: {
borderRadius: 24,
padding: 32,
backgroundColor: 'rgba(26, 26, 26, 0.8)',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
// backdropFilter: 'blur(10px)', // не поддерживается в React Native
// iOS тени
shadowColor: '#9333ea',
shadowColor: '#ffffff',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: 0.5,
shadowRadius: 20,
shadowOpacity: 0.1,
shadowRadius: 30,
// Android тень
elevation: 10,
elevation: 20,
},
title: {
textAlign: 'center',
marginBottom: 10,
fontSize: 32,
gradientBackground: {
position: 'absolute',
left: 0,
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',
color: '#ffffff',
textShadowColor: '#9333ea',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 10,
},
subtitle: {
textAlign: 'center',
fontSize: 16,
color: '#a855f7',
fontStyle: 'italic',
title: {
fontSize: 36,
fontWeight: '300',
color: '#ffffff',
letterSpacing: 4,
marginBottom: 8,
},
formContainer: {
width: '100%',
},
input: {
marginBottom: 20,
backgroundColor: '#1a1a2e',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
fontSize: 16,
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 тени
shadowColor: '#7c3aed',
shadowColor: '#ffffff',
shadowOffset: {
width: 0,
height: 4,
},
shadowOpacity: 0.3,
shadowRadius: 6,
shadowRadius: 12,
// Android тень
elevation: 5,
elevation: 8,
},
button: {
marginTop: 20,
marginBottom: 10,
borderRadius: 12,
backgroundColor: '#9333ea',
// iOS тени для кнопки
shadowColor: '#9333ea',
shadowOffset: {
width: 0,
height: 8,
disabledButton: {
opacity: 0.5,
},
shadowOpacity: 0.6,
shadowRadius: 15,
// Android тень
elevation: 10,
buttonText: {
color: '#000000',
fontSize: 16,
fontWeight: '700',
letterSpacing: 2,
},
buttonContent: {
paddingVertical: 8,
dividerContainer: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 24,
},
buttonLabel: {
fontSize: 18,
fontWeight: 'bold',
divider: {
flex: 1,
height: 1,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
},
dividerText: {
color: '#666666',
paddingHorizontal: 16,
fontSize: 12,
letterSpacing: 1,
},
linkButton: {
marginTop: 10,
flexDirection: 'row',
justifyContent: 'center',
paddingVertical: 12,
},
linkButtonLabel: {
fontSize: 16,
color: '#a855f7',
linkButtonText: {
fontSize: 14,
color: '#808080',
},
errorText: {
color: '#ef4444',
textAlign: 'center',
marginBottom: 10,
textShadowColor: '#ef4444',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 5,
linkButtonTextBold: {
fontSize: 14,
color: '#ffffff',
fontWeight: '600',
},
});

View File

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