diff --git a/frontend/App.tsx b/frontend/App.tsx index 3b28f26..669e849 100644 --- a/frontend/App.tsx +++ b/frontend/App.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { StatusBar } from 'expo-status-bar'; +import { Platform } from 'react-native'; import { ApolloProvider } from '@apollo/client'; import { Provider as PaperProvider, MD3DarkTheme, configureFonts } from 'react-native-paper'; import { SafeAreaProvider } from 'react-native-safe-area-context'; @@ -7,33 +8,53 @@ import { apolloClient } from './src/services/apollo-client'; import { AuthProvider } from './src/contexts/AuthContext'; import { AppNavigator } from './src/navigation/AppNavigator'; -// Кастомная темная тема в черно-фиолетовых тонах +// Современная черно-серая тема const theme = { ...MD3DarkTheme, colors: { ...MD3DarkTheme.colors, - primary: '#9333ea', - secondary: '#a855f7', - tertiary: '#7c3aed', - background: '#0a0a0f', - surface: '#1a1a2e', - surfaceVariant: '#2d2d42', + primary: '#ffffff', + secondary: '#b3b3b3', + tertiary: '#808080', + background: '#0a0a0a', + surface: '#1a1a1a', + surfaceVariant: '#2d2d2d', onSurface: '#ffffff', - onSurfaceVariant: '#e5e5e7', - onPrimary: '#ffffff', + onSurfaceVariant: '#e5e5e5', + onPrimary: '#000000', elevation: { level0: 'transparent', - level1: '#1a1a2e', - level2: '#2d2d42', - level3: '#3d3d56', - level4: '#4d4d6a', - level5: '#5d5d7e', + level1: '#1a1a1a', + level2: '#242424', + level3: '#2e2e2e', + level4: '#383838', + level5: '#424242', }, - outline: '#7c3aed', - outlineVariant: '#6d28d9', - error: '#ef4444', + 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: 12, + roundness: 16, + fonts: configureFonts({ + customVariant: { + fontFamily: Platform.select({ + ios: 'System', + android: 'Roboto', + default: 'sans-serif', + }), + fontWeight: '500', + letterSpacing: 0.5, + }, + }), }; export default function App() { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fe50e94..52b52c6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index 7738b04..16e9e6c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/src/components/BackgroundDesign.tsx b/frontend/src/components/BackgroundDesign.tsx new file mode 100644 index 0000000..b3a99af --- /dev/null +++ b/frontend/src/components/BackgroundDesign.tsx @@ -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 = ({ + 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 ( + + {/* Основной градиентный фон */} + + + {/* Декоративные элементы */} + + {/* Большой круг с градиентом слева вверху */} + + + + + {/* Плавающий элемент справа */} + + + + + {/* Вращающийся квадрат */} + + + + + {/* Сетка точек для login варианта */} + {variant === 'login' && ( + + {Array.from({ length: 10 }).map((_, i) => + Array.from({ length: 15 }).map((_, j) => ( + + )) + )} + + )} + + {/* Градиентные блики */} + + + + + + + {/* Контент поверх фона */} + + {children} + + + ); +}; + +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, + }, +}); \ No newline at end of file diff --git a/frontend/src/screens/ChatScreen.tsx b/frontend/src/screens/ChatScreen.tsx index c0dd24a..b2f474d 100644 --- a/frontend/src/screens/ChatScreen.tsx +++ b/frontend/src/screens/ChatScreen.tsx @@ -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 ( - + {!isOwnMessage && ( )} - - {!isOwnMessage && ( - - {item.sender.username} - + + {isOwnMessage ? ( + + + {item.content} + + + {messageTime} + {item.isEdited && ' • изменено'} + + + ) : ( + + + {item.sender.username} + + + {item.content} + + + {messageTime} + {item.isEdited && ' • изменено'} + + )} - - {item.content} - - - {messageTime} - {item.isEdited && ' • изменено'} - {isOwnMessage && ( { setSelectedMessage(null); }} anchor={ - { setSelectedMessage(item.id); setMenuVisible(true); }} style={styles.menuButton} - /> + > + + } + contentStyle={styles.menuContent} > - handleDeleteMessage(item.id)} title="Удалить" /> + handleDeleteMessage(item.id)} + title="Удалить" + titleStyle={styles.menuItemText} + /> )} - - + + ); }; if (loading && !data) { return ( - - Загрузка сообщений... + + Загрузка сообщений... ); } return ( - - item.id} - contentContainerStyle={styles.messagesList} - onContentSizeChange={() => flatListRef.current?.scrollToEnd()} + + - - - } + + item.id} + contentContainerStyle={styles.messagesList} + onContentSizeChange={() => flatListRef.current?.scrollToEnd()} + showsVerticalScrollIndicator={false} /> - - + + + + + } + /> + + + + ); }; 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, }, }); \ No newline at end of file diff --git a/frontend/src/screens/ConversationsScreen.tsx b/frontend/src/screens/ConversationsScreen.tsx index 733f721..bda8949 100644 --- a/frontend/src/screens/ConversationsScreen.tsx +++ b/frontend/src/screens/ConversationsScreen.tsx @@ -1,75 +1,138 @@ import React from 'react'; -import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native'; -import { List, Avatar, Text, FAB, Divider, Badge } from 'react-native-paper'; +import { View, StyleSheet, FlatList, TouchableOpacity, Dimensions } from 'react-native'; +import { List, Avatar, Text, FAB, Badge, Surface } from 'react-native-paper'; 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'; +import { LinearGradient } from 'expo-linear-gradient'; +import Animated, { + FadeInDown, + FadeInRight, + Layout, +} from 'react-native-reanimated'; + +const { width } = Dimensions.get('window'); export const ConversationsScreen = ({ navigation }: any) => { const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, { - pollInterval: 5000, // Обновляем каждые 5 секунд + pollInterval: 5000, }); - const renderConversation = ({ item }: { item: Conversation }) => { + 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 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) : ''; return ( - navigation.navigate('Chat', { conversationId: item.id, title: displayName })} + - ( - + navigation.navigate('Chat', { conversationId: item.id, title: displayName })} + activeOpacity={0.7} + > + + + {otherParticipant?.isOnline && ( - + )} - )} - right={() => ( - - - {lastMessageTime} - - {/* Здесь можно добавить счетчик непрочитанных сообщений */} + + + + + {displayName || 'Без имени'} + + + {lastMessageTime} + + + + + + {item.lastMessage?.content || 'Нет сообщений'} + + {/* Здесь можно добавить счетчик непрочитанных */} + - )} - style={styles.listItem} - /> - - + + + ); }; if (loading && !data) { return ( - - Загрузка... + + + Загрузка чатов... ); } if (error) { return ( - - Ошибка загрузки чатов + + + Ошибка загрузки чатов + refetch()} style={styles.retryButton}> + Попробовать снова + ); } return ( + { onRefresh={refetch} refreshing={loading} contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + ItemSeparatorComponent={() => } ListEmptyComponent={ - - - У вас пока нет чатов + + + + 💬 + + + Нет активных чатов - + Начните новый чат, нажав на кнопку внизу - + } /> - navigation.navigate('NewChat')} - /> + + navigation.navigate('NewChat')} + theme={{ + colors: { + primaryContainer: '#ffffff', + onPrimaryContainer: '#000000', + } + }} + /> + ); }; @@ -100,37 +185,133 @@ export const ConversationsScreen = ({ navigation }: any) => { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#ffffff', + backgroundColor: '#0a0a0a', }, - centerContainer: { + loadingContainer: { 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)', }, - time: { - color: '#666', + avatarContainer: { + position: 'relative', + marginRight: 12, + }, + avatarGradient: { + position: 'absolute', + width: 52, + height: 52, + borderRadius: 26, + }, + avatar: { + backgroundColor: '#2d2d2d', + }, + avatarLabel: { + color: '#ffffff', + fontSize: 20, + fontWeight: '600', }, 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, @@ -139,13 +320,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, }, }); \ No newline at end of file diff --git a/frontend/src/screens/LoginScreen.tsx b/frontend/src/screens/LoginScreen.tsx index 3e85c1c..cea14e4 100644 --- a/frontend/src/screens/LoginScreen.tsx +++ b/frontend/src/screens/LoginScreen.tsx @@ -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 { LOGIN } 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 LoginScreen = ({ navigation }: any) => { const [username, setUsername] = useState(''); @@ -26,25 +30,21 @@ export const LoginScreen = ({ 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); 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 @@ -66,24 +66,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]), }; }); @@ -93,21 +89,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); @@ -118,129 +100,150 @@ export const LoginScreen = ({ navigation }: any) => { }; return ( - - - - - Вход в Prism - Добро пожаловать обратно - - - - { - inputFocusAnimation1.value = withSpring(1); - }} - onBlur={() => { - inputFocusAnimation1.value = withSpring(0); - }} - /> - - - - setShowPassword(!showPassword)} - color="#a855f7" - /> - } - onFocus={() => { - inputFocusAnimation2.value = withSpring(1); - }} - onBlur={() => { - inputFocusAnimation2.value = withSpring(0); - }} - /> - - - {error && ( - - {error.message} - - )} - - - - - - - - - + + + + + + + P + + + PRISM + + + + + + setShowPassword(!showPassword)} + color="#808080" + /> + } + /> + + {error && ( + + {error.message} + + )} + + + + + + {loading ? 'ВХОД...' : 'ВОЙТИ'} + + + + + + + + ИЛИ + + + + navigation.navigate('Register')} + disabled={loading} + style={styles.linkButton} + activeOpacity={0.7} + > + + Нет аккаунта? + + + {' Создать'} + + + + + + + + ); }; const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: '#0a0a0f', }, scrollContent: { flexGrow: 1, @@ -253,90 +256,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, - }, - shadowOpacity: 0.6, - shadowRadius: 15, - // Android тень - elevation: 10, + disabledButton: { + opacity: 0.5, }, - buttonContent: { - paddingVertical: 8, + buttonText: { + color: '#000000', + fontSize: 16, + fontWeight: '700', + letterSpacing: 2, }, - buttonLabel: { - fontSize: 18, - fontWeight: 'bold', + dividerContainer: { + flexDirection: 'row', + 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, }, 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', }, }); \ No newline at end of file diff --git a/frontend/src/screens/RegisterScreen.tsx b/frontend/src/screens/RegisterScreen.tsx index 7b14f7d..8b53d6b 100644 --- a/frontend/src/screens/RegisterScreen.tsx +++ b/frontend/src/screens/RegisterScreen.tsx @@ -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,192 +104,196 @@ export const RegisterScreen = ({ navigation }: any) => { }; return ( - - - - - Регистрация в Prism - Присоединяйтесь к будущему - - - - { - inputFocusAnimation1.value = withSpring(1); - }} - onBlur={() => { - inputFocusAnimation1.value = withSpring(0); - }} - /> - - - - { - inputFocusAnimation2.value = withSpring(1); - }} - onBlur={() => { - inputFocusAnimation2.value = withSpring(0); - }} - /> - - - - setShowPassword(!showPassword)} - color="#a855f7" - /> - } - onFocus={() => { - inputFocusAnimation3.value = withSpring(1); - }} - onBlur={() => { - inputFocusAnimation3.value = withSpring(0); - }} - /> - - - - { - inputFocusAnimation4.value = withSpring(1); - }} - onBlur={() => { - inputFocusAnimation4.value = withSpring(0); - }} - /> - - - {!passwordsMatch && ( - - Пароли не совпадают - - )} - - {error && ( - - {error.message} - - )} - - - - - - - - - + + + + + РЕГИСТРАЦИЯ + + + + + + + + setShowPassword(!showPassword)} + color="#808080" + /> + } + underlineColor="transparent" + activeUnderlineColor="#ffffff" + /> + + + + {!passwordsMatch && confirmPassword !== '' && ( + + Пароли не совпадают + + )} + + {error && ( + + {error.message} + + )} + + + + + + {loading ? 'СОЗДАНИЕ...' : 'СОЗДАТЬ АККАУНТ'} + + + + + + + + ИЛИ + + + + navigation.navigate('Login')} + disabled={loading} + style={styles.linkButton} + activeOpacity={0.7} + > + + Уже есть аккаунт? + + + {' Войти'} + + + + + + + + ); }; 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, - }, - shadowOpacity: 0.6, - shadowRadius: 15, - // Android тень - elevation: 10, + disabledButton: { + opacity: 0.5, }, - buttonContent: { - paddingVertical: 8, + buttonText: { + color: '#000000', + fontSize: 16, + fontWeight: '700', + letterSpacing: 2, }, - buttonLabel: { - fontSize: 18, - fontWeight: 'bold', + dividerContainer: { + flexDirection: 'row', + 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, }, 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', }, }); \ No newline at end of file