Compare commits
2 Commits
a3ad9832ae
...
6a141d25b7
Author | SHA1 | Date | |
---|---|---|---|
6a141d25b7 | |||
020682854d |
@ -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 (
|
||||
|
169
frontend/package-lock.json
generated
169
frontend/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
252
frontend/src/components/BackgroundDesign.tsx
Normal file
252
frontend/src/components/BackgroundDesign.tsx
Normal 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,
|
||||
},
|
||||
});
|
@ -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,
|
||||
},
|
||||
});
|
@ -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,
|
||||
},
|
||||
});
|
@ -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',
|
||||
},
|
||||
});
|
@ -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',
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user