Compare commits
2 Commits
a3ad9832ae
...
6a141d25b7
Author | SHA1 | Date | |
---|---|---|---|
6a141d25b7 | |||
020682854d |
@ -1,12 +1,65 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { Platform } from 'react-native';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
import { ApolloProvider } from '@apollo/client';
|
||||||
import { Provider as PaperProvider } from 'react-native-paper';
|
import { Provider as PaperProvider } from 'react-native-paper';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import { apolloClient } from './src/services/apollo-client';
|
import { apolloClient } from './src/services/apollo-client';
|
||||||
import { AuthProvider } from './src/contexts/AuthContext';
|
import { AuthProvider } from './src/contexts/AuthContext';
|
||||||
import { AppNavigator } from './src/navigation/AppNavigator';
|
import { AppNavigator } from './src/navigation/AppNavigator';
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
|
// Современная черно-серая тема
|
||||||
|
const theme = {
|
||||||
|
...MD3DarkTheme,
|
||||||
|
colors: {
|
||||||
|
...MD3DarkTheme.colors,
|
||||||
|
primary: '#ffffff',
|
||||||
|
secondary: '#b3b3b3',
|
||||||
|
tertiary: '#808080',
|
||||||
|
background: '#0a0a0a',
|
||||||
|
surface: '#1a1a1a',
|
||||||
|
surfaceVariant: '#2d2d2d',
|
||||||
|
onSurface: '#ffffff',
|
||||||
|
onSurfaceVariant: '#e5e5e5',
|
||||||
|
onPrimary: '#000000',
|
||||||
|
elevation: {
|
||||||
|
level0: 'transparent',
|
||||||
|
level1: '#1a1a1a',
|
||||||
|
level2: '#242424',
|
||||||
|
level3: '#2e2e2e',
|
||||||
|
level4: '#383838',
|
||||||
|
level5: '#424242',
|
||||||
|
},
|
||||||
|
outline: '#666666',
|
||||||
|
outlineVariant: '#4d4d4d',
|
||||||
|
error: '#ff6b6b',
|
||||||
|
inverseSurface: '#e6e6e6',
|
||||||
|
inverseOnSurface: '#1a1a1a',
|
||||||
|
inversePrimary: '#000000',
|
||||||
|
backdrop: 'rgba(0, 0, 0, 0.8)',
|
||||||
|
notification: '#ffffff',
|
||||||
|
card: '#1a1a1a',
|
||||||
|
text: '#ffffff',
|
||||||
|
border: '#333333',
|
||||||
|
placeholder: '#808080',
|
||||||
|
},
|
||||||
|
roundness: 16,
|
||||||
|
fonts: configureFonts({
|
||||||
|
customVariant: {
|
||||||
|
fontFamily: Platform.select({
|
||||||
|
ios: 'System',
|
||||||
|
android: 'Roboto',
|
||||||
|
default: 'sans-serif',
|
||||||
|
}),
|
||||||
|
fontWeight: '500',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
=======
|
||||||
import { theme } from './src/theme';
|
import { theme } from './src/theme';
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
169
frontend/package-lock.json
generated
169
frontend/package-lock.json
generated
@ -16,6 +16,7 @@
|
|||||||
"@react-navigation/native-stack": "^7.3.24",
|
"@react-navigation/native-stack": "^7.3.24",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
|
"expo-linear-gradient": "^14.1.5",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"react-native-reanimated": "~3.17.4",
|
"react-native-reanimated": "~3.17.4",
|
||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
|
"react-native-svg": "15.11.2",
|
||||||
"react-native-vector-icons": "^10.3.0",
|
"react-native-vector-icons": "^10.3.0",
|
||||||
"react-native-web": "^0.20.0"
|
"react-native-web": "^0.20.0"
|
||||||
},
|
},
|
||||||
@ -3425,6 +3427,12 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||||
@ -3997,6 +4005,56 @@
|
|||||||
"hyphenate-style-name": "^1.0.3"
|
"hyphenate-style-name": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.0.14",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
@ -4110,6 +4168,61 @@
|
|||||||
"node": ">=0.10"
|
"node": ">=0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.4.7",
|
"version": "16.4.7",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
|
||||||
@ -4170,6 +4283,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/env-editor": {
|
"node_modules/env-editor": {
|
||||||
"version": "0.4.2",
|
"version": "0.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
|
||||||
@ -4371,6 +4496,17 @@
|
|||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-linear-gradient": {
|
||||||
|
"version": "14.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.1.5.tgz",
|
||||||
|
"integrity": "sha512-BSN3MkSGLZoHMduEnAgfhoj3xqcDWaoICgIr4cIYEx1GcHfKMhzA/O4mpZJ/WC27BP1rnAqoKfbclk1eA70ndQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-modules-autolinking": {
|
"node_modules/expo-modules-autolinking": {
|
||||||
"version": "2.1.14",
|
"version": "2.1.14",
|
||||||
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz",
|
"resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.14.tgz",
|
||||||
@ -5733,6 +5869,12 @@
|
|||||||
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||||
|
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@ -6343,6 +6485,18 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nullthrows": {
|
"node_modules/nullthrows": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||||
@ -7221,6 +7375,21 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-svg": {
|
||||||
|
"version": "15.11.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.11.2.tgz",
|
||||||
|
"integrity": "sha512-+YfF72IbWQUKzCIydlijV1fLuBsQNGMT6Da2kFlo1sh+LE3BIm/2Q7AR1zAAR6L0BFLi1WaQPLfFUC9bNZpOmw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-tree": "^1.1.3",
|
||||||
|
"warn-once": "0.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-vector-icons": {
|
"node_modules/react-native-vector-icons": {
|
||||||
"version": "10.3.0",
|
"version": "10.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-10.3.0.tgz",
|
||||||
|
@ -10,15 +10,18 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apollo/client": "^3.13.9",
|
"@apollo/client": "^3.13.9",
|
||||||
|
"@expo/metro-runtime": "~5.0.4",
|
||||||
"@react-native-async-storage/async-storage": "2.1.2",
|
"@react-native-async-storage/async-storage": "2.1.2",
|
||||||
"@react-navigation/bottom-tabs": "^7.4.5",
|
"@react-navigation/bottom-tabs": "^7.4.5",
|
||||||
"@react-navigation/native": "^7.1.17",
|
"@react-navigation/native": "^7.1.17",
|
||||||
"@react-navigation/native-stack": "^7.3.24",
|
"@react-navigation/native-stack": "^7.3.24",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"expo": "~53.0.20",
|
"expo": "~53.0.20",
|
||||||
|
"expo-linear-gradient": "^14.1.5",
|
||||||
"expo-status-bar": "~2.2.3",
|
"expo-status-bar": "~2.2.3",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"react": "19.0.0",
|
"react": "19.0.0",
|
||||||
|
"react-dom": "19.0.0",
|
||||||
"react-native": "0.79.5",
|
"react-native": "0.79.5",
|
||||||
"react-native-gesture-handler": "~2.24.0",
|
"react-native-gesture-handler": "~2.24.0",
|
||||||
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
"react-native-keyboard-aware-scroll-view": "^0.9.5",
|
||||||
@ -27,9 +30,8 @@
|
|||||||
"react-native-safe-area-context": "5.4.0",
|
"react-native-safe-area-context": "5.4.0",
|
||||||
"react-native-screens": "~4.11.1",
|
"react-native-screens": "~4.11.1",
|
||||||
"react-native-vector-icons": "^10.3.0",
|
"react-native-vector-icons": "^10.3.0",
|
||||||
"react-dom": "19.0.0",
|
|
||||||
"react-native-web": "^0.20.0",
|
"react-native-web": "^0.20.0",
|
||||||
"@expo/metro-runtime": "~5.0.4"
|
"react-native-svg": "15.11.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.25.2",
|
"@babel/core": "^7.25.2",
|
||||||
|
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 React, { useState, useRef, useEffect } from 'react';
|
||||||
import { View, StyleSheet, FlatList, KeyboardAvoidingView, Platform } from 'react-native';
|
import { View, StyleSheet, FlatList, KeyboardAvoidingView, Platform, TouchableOpacity } from 'react-native';
|
||||||
import { TextInput, IconButton, Text, Avatar, Surface, Menu } from 'react-native-paper';
|
import { TextInput, IconButton, Text, Avatar, Surface, Menu } from 'react-native-paper';
|
||||||
import { useQuery, useMutation, useSubscription } from '@apollo/client';
|
import { useQuery, useMutation, useSubscription } from '@apollo/client';
|
||||||
import { GET_MESSAGES } from '../graphql/queries';
|
import { GET_MESSAGES } from '../graphql/queries';
|
||||||
@ -9,6 +9,14 @@ import { Message } from '../types';
|
|||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { ru } from 'date-fns/locale';
|
import { ru } from 'date-fns/locale';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import Animated, {
|
||||||
|
FadeInDown,
|
||||||
|
FadeOutDown,
|
||||||
|
Layout,
|
||||||
|
SlideInRight,
|
||||||
|
SlideInLeft,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
export const ChatScreen = ({ route }: any) => {
|
export const ChatScreen = ({ route }: any) => {
|
||||||
const { conversationId, title } = route.params;
|
const { conversationId, title } = route.params;
|
||||||
@ -61,7 +69,6 @@ export const ChatScreen = ({ route }: any) => {
|
|||||||
variables: { conversationId },
|
variables: { conversationId },
|
||||||
onData: ({ data }) => {
|
onData: ({ data }) => {
|
||||||
if (data?.data?.messageAdded && data.data.messageAdded.sender.id !== user?.id) {
|
if (data?.data?.messageAdded && data.data.messageAdded.sender.id !== user?.id) {
|
||||||
// Сообщение от другого пользователя
|
|
||||||
flatListRef.current?.scrollToEnd();
|
flatListRef.current?.scrollToEnd();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -94,35 +101,60 @@ export const ChatScreen = ({ route }: any) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMessage = ({ item }: { item: Message }) => {
|
const renderMessage = ({ item, index }: { item: Message; index: number }) => {
|
||||||
const isOwnMessage = item.sender.id === user?.id;
|
const isOwnMessage = item.sender.id === user?.id;
|
||||||
const messageTime = format(new Date(item.createdAt), 'HH:mm', { locale: ru });
|
const messageTime = format(new Date(item.createdAt), 'HH:mm', { locale: ru });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[styles.messageContainer, isOwnMessage && styles.ownMessageContainer]}>
|
<Animated.View
|
||||||
{!isOwnMessage && (
|
entering={isOwnMessage ? SlideInRight.delay(index * 50) : SlideInLeft.delay(index * 50)}
|
||||||
<Avatar.Text
|
exiting={FadeOutDown}
|
||||||
size={36}
|
layout={Layout.springify()}
|
||||||
label={item.sender.username.charAt(0).toUpperCase()}
|
style={[styles.messageContainer, isOwnMessage && styles.ownMessageContainer]}
|
||||||
style={styles.avatar}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Surface
|
|
||||||
style={[styles.messageBubble, isOwnMessage && styles.ownMessageBubble]}
|
|
||||||
elevation={1}
|
|
||||||
>
|
>
|
||||||
{!isOwnMessage && (
|
{!isOwnMessage && (
|
||||||
<Text variant="labelSmall" style={styles.senderName}>
|
<Avatar.Text
|
||||||
{item.sender.username}
|
size={40}
|
||||||
</Text>
|
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}
|
{item.content}
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="bodySmall" style={[styles.messageTime, isOwnMessage && styles.ownMessageTime]}>
|
<Text variant="bodySmall" style={[styles.messageTime, styles.ownMessageTime]}>
|
||||||
{messageTime}
|
{messageTime}
|
||||||
{item.isEdited && ' • изменено'}
|
{item.isEdited && ' • изменено'}
|
||||||
</Text>
|
</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 && (
|
{isOwnMessage && (
|
||||||
<Menu
|
<Menu
|
||||||
visible={menuVisible && selectedMessage === item.id}
|
visible={menuVisible && selectedMessage === item.id}
|
||||||
@ -131,36 +163,50 @@ export const ChatScreen = ({ route }: any) => {
|
|||||||
setSelectedMessage(null);
|
setSelectedMessage(null);
|
||||||
}}
|
}}
|
||||||
anchor={
|
anchor={
|
||||||
<IconButton
|
<TouchableOpacity
|
||||||
icon="dots-vertical"
|
|
||||||
size={16}
|
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setSelectedMessage(item.id);
|
setSelectedMessage(item.id);
|
||||||
setMenuVisible(true);
|
setMenuVisible(true);
|
||||||
}}
|
}}
|
||||||
style={styles.menuButton}
|
style={styles.menuButton}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<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>
|
</Menu>
|
||||||
)}
|
)}
|
||||||
</Surface>
|
|
||||||
</View>
|
</View>
|
||||||
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && !data) {
|
if (loading && !data) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<Text>Загрузка сообщений...</Text>
|
<Text style={styles.loadingText}>Загрузка сообщений...</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#0a0a0a', '#1a1a1a']}
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
/>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={styles.container}
|
style={styles.keyboardAvoidingView}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
keyboardVerticalOffset={90}
|
keyboardVerticalOffset={90}
|
||||||
>
|
>
|
||||||
@ -171,38 +217,65 @@ export const ChatScreen = ({ route }: any) => {
|
|||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
contentContainerStyle={styles.messagesList}
|
contentContainerStyle={styles.messagesList}
|
||||||
onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
|
onContentSizeChange={() => flatListRef.current?.scrollToEnd()}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
/>
|
/>
|
||||||
<View style={styles.inputContainer}>
|
<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
|
<TextInput
|
||||||
value={message}
|
value={message}
|
||||||
onChangeText={setMessage}
|
onChangeText={setMessage}
|
||||||
placeholder="Введите сообщение..."
|
placeholder="Введите сообщение..."
|
||||||
|
placeholderTextColor="#666666"
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
multiline
|
multiline
|
||||||
maxLength={1000}
|
maxLength={1000}
|
||||||
|
theme={{
|
||||||
|
colors: {
|
||||||
|
primary: '#ffffff',
|
||||||
|
placeholder: '#666666',
|
||||||
|
text: '#ffffff',
|
||||||
|
background: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
outline: '#333333',
|
||||||
|
}
|
||||||
|
}}
|
||||||
right={
|
right={
|
||||||
<TextInput.Icon
|
<TextInput.Icon
|
||||||
icon="send"
|
icon="send"
|
||||||
onPress={handleSend}
|
onPress={handleSend}
|
||||||
disabled={!message.trim()}
|
disabled={!message.trim()}
|
||||||
|
color={message.trim() ? '#ffffff' : '#666666'}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#0a0a0a',
|
||||||
},
|
},
|
||||||
centerContainer: {
|
keyboardAvoidingView: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#0a0a0a',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
color: '#666666',
|
||||||
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
messagesList: {
|
messagesList: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
@ -217,47 +290,87 @@ const styles = StyleSheet.create({
|
|||||||
justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
},
|
},
|
||||||
avatar: {
|
avatar: {
|
||||||
marginRight: 8,
|
marginRight: 12,
|
||||||
|
backgroundColor: '#2d2d2d',
|
||||||
|
},
|
||||||
|
avatarLabel: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 18,
|
||||||
|
},
|
||||||
|
messageBubbleContainer: {
|
||||||
|
maxWidth: '75%',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
ownMessageBubbleContainer: {
|
||||||
|
alignItems: 'flex-end',
|
||||||
},
|
},
|
||||||
messageBubble: {
|
messageBubble: {
|
||||||
maxWidth: '75%',
|
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 16,
|
paddingRight: 16,
|
||||||
backgroundColor: '#fff',
|
borderRadius: 18,
|
||||||
|
minWidth: 80,
|
||||||
},
|
},
|
||||||
ownMessageBubble: {
|
otherMessageBubble: {
|
||||||
backgroundColor: '#2196F3',
|
backgroundColor: '#1a1a1a',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
},
|
},
|
||||||
senderName: {
|
senderName: {
|
||||||
color: '#666',
|
color: '#808080',
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
|
fontSize: 12,
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
messageText: {
|
messageText: {
|
||||||
color: '#000',
|
color: '#ffffff',
|
||||||
|
fontSize: 15,
|
||||||
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
ownMessageText: {
|
ownMessageText: {
|
||||||
color: '#fff',
|
color: '#000000',
|
||||||
},
|
},
|
||||||
messageTime: {
|
messageTime: {
|
||||||
color: '#666',
|
color: '#666666',
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
},
|
},
|
||||||
ownMessageTime: {
|
ownMessageTime: {
|
||||||
color: 'rgba(255, 255, 255, 0.7)',
|
color: 'rgba(0, 0, 0, 0.5)',
|
||||||
},
|
},
|
||||||
menuButton: {
|
menuButton: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: -8,
|
top: -8,
|
||||||
right: -8,
|
right: -40,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
menuContent: {
|
||||||
|
backgroundColor: '#1a1a1a',
|
||||||
|
borderRadius: 12,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
menuItemText: {
|
||||||
|
color: '#ffffff',
|
||||||
},
|
},
|
||||||
inputContainer: {
|
inputContainer: {
|
||||||
padding: 8,
|
position: 'relative',
|
||||||
backgroundColor: '#fff',
|
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#e0e0e0',
|
borderTopColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
inputGradient: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
inputWrapper: {
|
||||||
|
padding: 12,
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
maxHeight: 100,
|
maxHeight: 100,
|
||||||
|
fontSize: 16,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderRadius: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
@ -1,12 +1,29 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
|
import React from 'react';
|
||||||
|
import { View, StyleSheet, FlatList, TouchableOpacity, Dimensions } from 'react-native';
|
||||||
|
import { List, Avatar, Text, FAB, Badge, Surface } from 'react-native-paper';
|
||||||
|
=======
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
import { View, StyleSheet, FlatList, TouchableOpacity } from 'react-native';
|
||||||
import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton, useTheme } from 'react-native-paper';
|
import { List, Avatar, Text, FAB, Divider, Badge, Searchbar, IconButton, useTheme } from 'react-native-paper';
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
import { useQuery } from '@apollo/client';
|
import { useQuery } from '@apollo/client';
|
||||||
import { GET_CONVERSATIONS } from '../graphql/queries';
|
import { GET_CONVERSATIONS } from '../graphql/queries';
|
||||||
import { Conversation } from '../types';
|
import { Conversation } from '../types';
|
||||||
import { format } from 'date-fns';
|
import { format, isToday, isYesterday } from 'date-fns';
|
||||||
import { ru } from 'date-fns/locale';
|
import { ru } from 'date-fns/locale';
|
||||||
|
<<<<<<< HEAD
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import Animated, {
|
||||||
|
FadeInDown,
|
||||||
|
FadeInRight,
|
||||||
|
Layout,
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
=======
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
|
|
||||||
export const ConversationsScreen = ({ navigation }: any) => {
|
export const ConversationsScreen = ({ navigation }: any) => {
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
@ -14,9 +31,24 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, {
|
const { data, loading, error, refetch } = useQuery(GET_CONVERSATIONS, {
|
||||||
pollInterval: 5000, // Обновляем каждые 5 секунд
|
pollInterval: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
|
const formatMessageTime = (date: string) => {
|
||||||
|
const messageDate = new Date(date);
|
||||||
|
if (isToday(messageDate)) {
|
||||||
|
return format(messageDate, 'HH:mm', { locale: ru });
|
||||||
|
} else if (isYesterday(messageDate)) {
|
||||||
|
return 'Вчера';
|
||||||
|
} else {
|
||||||
|
return format(messageDate, 'dd.MM', { locale: ru });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderConversation = ({ item, index }: { item: Conversation; index: number }) => {
|
||||||
|
const otherParticipant = item.participants.find(p => p.id !== data?.me?.id);
|
||||||
|
=======
|
||||||
// Фильтрация чатов по поисковому запросу
|
// Фильтрация чатов по поисковому запросу
|
||||||
const filteredConversations = data?.conversations?.filter((conv: Conversation) => {
|
const filteredConversations = data?.conversations?.filter((conv: Conversation) => {
|
||||||
if (!searchQuery) return true;
|
if (!searchQuery) return true;
|
||||||
@ -30,31 +62,72 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
|
|
||||||
const renderConversation = ({ item }: { item: Conversation }) => {
|
const renderConversation = ({ item }: { item: Conversation }) => {
|
||||||
const otherParticipant = item.participants.find(p => p.id !== user?.id);
|
const otherParticipant = item.participants.find(p => p.id !== user?.id);
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
const displayName = item.isGroup ? item.name : otherParticipant?.username;
|
const displayName = item.isGroup ? item.name : otherParticipant?.username;
|
||||||
const lastMessageTime = item.lastMessage
|
const lastMessageTime = item.lastMessage
|
||||||
? format(new Date(item.lastMessage.createdAt), 'HH:mm', { locale: ru })
|
? formatMessageTime(item.lastMessage.createdAt)
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
// Подсчет непрочитанных сообщений (в будущем добавить в GraphQL)
|
// Подсчет непрочитанных сообщений (в будущем добавить в GraphQL)
|
||||||
const unreadCount = 0;
|
const unreadCount = 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.delay(index * 50).springify()}
|
||||||
|
layout={Layout.springify()}
|
||||||
|
>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={() => navigation.navigate('Chat', { conversationId: item.id, title: displayName })}
|
onPress={() => navigation.navigate('Chat', { conversationId: item.id, title: displayName })}
|
||||||
|
activeOpacity={0.7}
|
||||||
>
|
>
|
||||||
<List.Item
|
<Surface style={styles.conversationItem} elevation={0}>
|
||||||
title={displayName || 'Без имени'}
|
<View style={styles.avatarContainer}>
|
||||||
description={item.lastMessage?.content || 'Нет сообщений'}
|
<LinearGradient
|
||||||
left={() => (
|
colors={['rgba(255,255,255,0.1)', 'rgba(255,255,255,0.05)']}
|
||||||
<View>
|
style={styles.avatarGradient}
|
||||||
|
/>
|
||||||
<Avatar.Text
|
<Avatar.Text
|
||||||
size={50}
|
size={52}
|
||||||
label={displayName?.charAt(0).toUpperCase() || '?'}
|
label={displayName?.charAt(0).toUpperCase() || '?'}
|
||||||
|
style={styles.avatar}
|
||||||
|
labelStyle={styles.avatarLabel}
|
||||||
|
theme={{
|
||||||
|
colors: {
|
||||||
|
primary: '#2d2d2d',
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{otherParticipant?.isOnline && (
|
{otherParticipant?.isOnline && (
|
||||||
<Badge style={styles.onlineBadge} size={12} />
|
<Badge style={styles.onlineBadge} size={14} />
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
|
||||||
|
<View style={styles.contentContainer}>
|
||||||
|
<View style={styles.headerRow}>
|
||||||
|
<Text
|
||||||
|
variant="titleMedium"
|
||||||
|
style={styles.conversationTitle}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{displayName || 'Без имени'}
|
||||||
|
</Text>
|
||||||
|
<Text variant="bodySmall" style={styles.time}>
|
||||||
|
{lastMessageTime}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.messageRow}>
|
||||||
|
<Text
|
||||||
|
variant="bodyMedium"
|
||||||
|
style={styles.lastMessage}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{item.lastMessage?.content || 'Нет сообщений'}
|
||||||
|
</Text>
|
||||||
|
{/* Здесь можно добавить счетчик непрочитанных */}
|
||||||
|
</View>
|
||||||
|
=======
|
||||||
)}
|
)}
|
||||||
right={() => (
|
right={() => (
|
||||||
<View style={styles.rightContent}>
|
<View style={styles.rightContent}>
|
||||||
@ -64,32 +137,49 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
{unreadCount > 0 && (
|
{unreadCount > 0 && (
|
||||||
<Badge style={styles.unreadBadge}>{unreadCount}</Badge>
|
<Badge style={styles.unreadBadge}>{unreadCount}</Badge>
|
||||||
)}
|
)}
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
</View>
|
</View>
|
||||||
)}
|
</Surface>
|
||||||
style={styles.listItem}
|
|
||||||
/>
|
|
||||||
<Divider />
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (loading && !data) {
|
if (loading && !data) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.loadingContainer}>
|
||||||
<Text>Загрузка...</Text>
|
<LinearGradient
|
||||||
|
colors={['#0a0a0a', '#1a1a1a']}
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
/>
|
||||||
|
<Text style={styles.loadingText}>Загрузка чатов...</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.centerContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text>Ошибка загрузки чатов</Text>
|
<LinearGradient
|
||||||
|
colors={['#0a0a0a', '#1a1a1a']}
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
/>
|
||||||
|
<Text style={styles.errorText}>Ошибка загрузки чатов</Text>
|
||||||
|
<TouchableOpacity onPress={() => refetch()} style={styles.retryButton}>
|
||||||
|
<Text style={styles.retryText}>Попробовать снова</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<View style={styles.container}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#0a0a0a', '#1a1a1a']}
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
/>
|
||||||
|
=======
|
||||||
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
|
<View style={[styles.container, { backgroundColor: theme.colors.background }]}>
|
||||||
{/* Поисковая строка */}
|
{/* Поисковая строка */}
|
||||||
<View style={[styles.searchContainer, { backgroundColor: theme.colors.surface }]}>
|
<View style={[styles.searchContainer, { backgroundColor: theme.colors.surface }]}>
|
||||||
@ -102,6 +192,7 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
<FlatList
|
<FlatList
|
||||||
data={filteredConversations}
|
data={filteredConversations}
|
||||||
renderItem={renderConversation}
|
renderItem={renderConversation}
|
||||||
@ -109,22 +200,52 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
onRefresh={refetch}
|
onRefresh={refetch}
|
||||||
refreshing={loading}
|
refreshing={loading}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.emptyContainer}>
|
<Animated.View
|
||||||
<Text variant="bodyLarge" style={styles.emptyText}>
|
style={styles.emptyContainer}
|
||||||
У вас пока нет чатов
|
entering={FadeInDown.duration(600)}
|
||||||
|
>
|
||||||
|
<View style={styles.emptyIconContainer}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['rgba(255,255,255,0.05)', 'rgba(255,255,255,0.02)']}
|
||||||
|
style={styles.emptyIconGradient}
|
||||||
|
/>
|
||||||
|
<Text style={styles.emptyIcon}>💬</Text>
|
||||||
|
</View>
|
||||||
|
<Text variant="headlineSmall" style={styles.emptyText}>
|
||||||
|
Нет активных чатов
|
||||||
</Text>
|
</Text>
|
||||||
<Text variant="bodyMedium" style={styles.emptySubtext}>
|
<Text variant="bodyLarge" style={styles.emptySubtext}>
|
||||||
Начните новый чат, нажав на кнопку внизу
|
Начните новый чат, нажав на кнопку внизу
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</Animated.View>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInRight.delay(300).springify()}
|
||||||
|
>
|
||||||
|
<FAB
|
||||||
|
icon="plus"
|
||||||
|
style={styles.fab}
|
||||||
|
onPress={() => navigation.navigate('NewChat')}
|
||||||
|
theme={{
|
||||||
|
colors: {
|
||||||
|
primaryContainer: '#ffffff',
|
||||||
|
onPrimaryContainer: '#000000',
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
=======
|
||||||
<FAB
|
<FAB
|
||||||
icon="plus"
|
icon="plus"
|
||||||
style={styles.fab}
|
style={styles.fab}
|
||||||
onPress={() => navigation.navigate('Contacts')}
|
onPress={() => navigation.navigate('Contacts')}
|
||||||
/>
|
/>
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -132,8 +253,11 @@ export const ConversationsScreen = ({ navigation }: any) => {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#0a0a0a',
|
||||||
},
|
},
|
||||||
|
<<<<<<< HEAD
|
||||||
|
loadingContainer: {
|
||||||
|
=======
|
||||||
searchContainer: {
|
searchContainer: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
paddingBottom: 8,
|
paddingBottom: 8,
|
||||||
@ -145,20 +269,72 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: '#f5f5f5',
|
||||||
},
|
},
|
||||||
centerContainer: {
|
centerContainer: {
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#0a0a0a',
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
color: '#666666',
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: '#0a0a0a',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#ff6b6b',
|
||||||
|
fontSize: 16,
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
retryButton: {
|
||||||
|
paddingHorizontal: 24,
|
||||||
|
paddingVertical: 12,
|
||||||
|
borderRadius: 24,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
retryText: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
listContent: {
|
listContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
|
||||||
listItem: {
|
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
},
|
},
|
||||||
rightContent: {
|
conversationItem: {
|
||||||
alignItems: 'flex-end',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
marginHorizontal: 12,
|
||||||
|
marginVertical: 4,
|
||||||
|
borderRadius: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
},
|
},
|
||||||
|
<<<<<<< HEAD
|
||||||
|
avatarContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
avatarGradient: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 52,
|
||||||
|
height: 52,
|
||||||
|
borderRadius: 26,
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
backgroundColor: '#2d2d2d',
|
||||||
|
},
|
||||||
|
avatarLabel: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: '600',
|
||||||
|
=======
|
||||||
time: {
|
time: {
|
||||||
color: '#666',
|
color: '#666',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
@ -168,18 +344,68 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: '#2196F3',
|
backgroundColor: '#2196F3',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
},
|
},
|
||||||
onlineBadge: {
|
onlineBadge: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
backgroundColor: '#4CAF50',
|
backgroundColor: '#4CAF50',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#0a0a0a',
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
headerRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
conversationTitle: {
|
||||||
|
color: '#ffffff',
|
||||||
|
fontWeight: '600',
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
time: {
|
||||||
|
color: '#666666',
|
||||||
|
fontSize: 12,
|
||||||
|
},
|
||||||
|
messageRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
lastMessage: {
|
||||||
|
color: '#999999',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
separator: {
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
marginHorizontal: 28,
|
||||||
|
marginVertical: 4,
|
||||||
},
|
},
|
||||||
fab: {
|
fab: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
margin: 16,
|
margin: 16,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderRadius: 16,
|
||||||
|
// iOS тени
|
||||||
|
shadowColor: '#ffffff',
|
||||||
|
shadowOffset: {
|
||||||
|
width: 0,
|
||||||
|
height: 4,
|
||||||
|
},
|
||||||
|
shadowOpacity: 0.3,
|
||||||
|
shadowRadius: 12,
|
||||||
|
// Android тень
|
||||||
|
elevation: 8,
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -188,13 +414,36 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 40,
|
paddingHorizontal: 40,
|
||||||
paddingTop: 100,
|
paddingTop: 100,
|
||||||
},
|
},
|
||||||
|
emptyIconContainer: {
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 24,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.03)',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
},
|
||||||
|
emptyIconGradient: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 120,
|
||||||
|
height: 120,
|
||||||
|
borderRadius: 60,
|
||||||
|
},
|
||||||
|
emptyIcon: {
|
||||||
|
fontSize: 48,
|
||||||
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 12,
|
||||||
color: '#666',
|
color: '#ffffff',
|
||||||
|
fontWeight: '300',
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
emptySubtext: {
|
emptySubtext: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
color: '#999',
|
color: '#666666',
|
||||||
|
maxWidth: 250,
|
||||||
},
|
},
|
||||||
});
|
});
|
@ -1,9 +1,15 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
|
<<<<<<< HEAD
|
||||||
|
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableOpacity } from 'react-native';
|
||||||
|
import { TextInput, Button, Text, Headline, HelperText, Surface } from 'react-native-paper';
|
||||||
|
=======
|
||||||
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
|
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
|
||||||
import { TextInput, Button, Text, Headline, HelperText, useTheme } from 'react-native-paper';
|
import { TextInput, Button, Text, Headline, HelperText, useTheme } from 'react-native-paper';
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { LOGIN } from '../graphql/mutations';
|
import { LOGIN } from '../graphql/mutations';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { BackgroundDesign } from '../components/BackgroundDesign';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@ -14,10 +20,13 @@ import Animated, {
|
|||||||
withRepeat,
|
withRepeat,
|
||||||
interpolate,
|
interpolate,
|
||||||
Easing,
|
Easing,
|
||||||
|
FadeIn,
|
||||||
|
FadeInDown,
|
||||||
|
FadeInUp,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||||
const AnimatedView = Animated.View;
|
|
||||||
|
|
||||||
export const LoginScreen = ({ navigation }: any) => {
|
export const LoginScreen = ({ navigation }: any) => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@ -27,25 +36,21 @@ export const LoginScreen = ({ navigation }: any) => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
// Анимации
|
// Анимации
|
||||||
const translateY = useSharedValue(50);
|
const cardScale = useSharedValue(0.95);
|
||||||
const opacity = useSharedValue(0);
|
const cardOpacity = useSharedValue(0);
|
||||||
const scale = useSharedValue(0.9);
|
|
||||||
const glowAnimation = useSharedValue(0);
|
const glowAnimation = useSharedValue(0);
|
||||||
const buttonScale = useSharedValue(1);
|
const buttonScale = useSharedValue(1);
|
||||||
const inputFocusAnimation1 = useSharedValue(0);
|
|
||||||
const inputFocusAnimation2 = useSharedValue(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Анимация появления
|
// Анимация появления карточки
|
||||||
translateY.value = withSpring(0, { damping: 15, stiffness: 100 });
|
cardScale.value = withSpring(1, { damping: 15, stiffness: 100 });
|
||||||
opacity.value = withTiming(1, { duration: 800 });
|
cardOpacity.value = withTiming(1, { duration: 800 });
|
||||||
scale.value = withSpring(1, { damping: 15, stiffness: 100 });
|
|
||||||
|
|
||||||
// Пульсирующее свечение
|
// Мягкое свечение
|
||||||
glowAnimation.value = withRepeat(
|
glowAnimation.value = withRepeat(
|
||||||
withSequence(
|
withSequence(
|
||||||
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.ease) }),
|
||||||
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) })
|
withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.ease) })
|
||||||
),
|
),
|
||||||
-1,
|
-1,
|
||||||
false
|
false
|
||||||
@ -67,24 +72,20 @@ export const LoginScreen = ({ navigation }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Анимированные стили
|
// Анимированные стили
|
||||||
const containerAnimatedStyle = useAnimatedStyle(() => {
|
const cardAnimatedStyle = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [{ scale: cardScale.value }],
|
||||||
{ translateY: translateY.value },
|
opacity: cardOpacity.value,
|
||||||
{ scale: scale.value }
|
|
||||||
],
|
|
||||||
opacity: opacity.value,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const glowContainerStyle = useAnimatedStyle(() => {
|
const glowStyle = useAnimatedStyle(() => {
|
||||||
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]);
|
const shadowOpacity = interpolate(glowAnimation.value, [0, 1], [0.1, 0.3]);
|
||||||
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]);
|
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [20, 40]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shadowOpacity: glowOpacity,
|
shadowOpacity: shadowOpacity,
|
||||||
shadowRadius: shadowRadius,
|
shadowRadius: shadowRadius,
|
||||||
elevation: interpolate(glowAnimation.value, [0, 1], [5, 15]),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -94,21 +95,7 @@ export const LoginScreen = ({ navigation }: any) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const createInputAnimatedStyle = (focusAnimation: any) => {
|
|
||||||
return useAnimatedStyle(() => {
|
|
||||||
const borderWidth = interpolate(focusAnimation.value, [0, 1], [1, 2]);
|
|
||||||
const shadowOpacity = interpolate(focusAnimation.value, [0, 1], [0, 0.6]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
borderWidth: borderWidth,
|
|
||||||
shadowOpacity: shadowOpacity,
|
|
||||||
elevation: interpolate(focusAnimation.value, [0, 1], [2, 8]),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputStyle1 = createInputAnimatedStyle(inputFocusAnimation1);
|
|
||||||
const inputStyle2 = createInputAnimatedStyle(inputFocusAnimation2);
|
|
||||||
|
|
||||||
const handleButtonPressIn = () => {
|
const handleButtonPressIn = () => {
|
||||||
buttonScale.value = withSpring(0.95);
|
buttonScale.value = withSpring(0.95);
|
||||||
@ -119,6 +106,17 @@ export const LoginScreen = ({ navigation }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<<<<<<< HEAD
|
||||||
|
<BackgroundDesign variant="login">
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.container}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
>
|
||||||
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
|
<Animated.View
|
||||||
|
style={[styles.content, cardAnimatedStyle]}
|
||||||
|
entering={FadeInDown.duration(800).springify()}
|
||||||
|
=======
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={[styles.container, { backgroundColor: theme.colors.background }]}
|
style={[styles.container, { backgroundColor: theme.colors.background }]}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
@ -229,19 +227,142 @@ export const LoginScreen = ({ navigation }: any) => {
|
|||||||
primary: '#a855f7',
|
primary: '#a855f7',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
>>>>>>> a3ad9832ae1663e2a76b50c417d43bcb23a0e03a
|
||||||
>
|
>
|
||||||
Нет аккаунта? Зарегистрироваться
|
<Animated.View style={[styles.loginCard, glowStyle]}>
|
||||||
</Button>
|
<LinearGradient
|
||||||
|
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>
|
</Animated.View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
</BackgroundDesign>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#0a0a0f',
|
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
@ -254,90 +375,135 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
glowContainer: {
|
loginCard: {
|
||||||
marginBottom: 40,
|
borderRadius: 24,
|
||||||
padding: 20,
|
padding: 32,
|
||||||
borderRadius: 20,
|
backgroundColor: 'rgba(26, 26, 26, 0.8)',
|
||||||
backgroundColor: '#1a1a2e',
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
// backdropFilter: 'blur(10px)', // не поддерживается в React Native
|
||||||
// iOS тени
|
// iOS тени
|
||||||
shadowColor: '#9333ea',
|
shadowColor: '#ffffff',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.5,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 20,
|
shadowRadius: 30,
|
||||||
// Android тень
|
// Android тень
|
||||||
elevation: 10,
|
elevation: 20,
|
||||||
},
|
},
|
||||||
title: {
|
gradientBackground: {
|
||||||
textAlign: 'center',
|
position: 'absolute',
|
||||||
marginBottom: 10,
|
left: 0,
|
||||||
fontSize: 32,
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: 24,
|
||||||
|
},
|
||||||
|
headerContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 40,
|
||||||
|
},
|
||||||
|
logoContainer: {
|
||||||
|
marginBottom: 20,
|
||||||
|
},
|
||||||
|
logoPlaceholder: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
},
|
||||||
|
logoText: {
|
||||||
|
fontSize: 40,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
textShadowColor: '#9333ea',
|
|
||||||
textShadowOffset: { width: 0, height: 0 },
|
|
||||||
textShadowRadius: 10,
|
|
||||||
},
|
},
|
||||||
subtitle: {
|
title: {
|
||||||
textAlign: 'center',
|
fontSize: 36,
|
||||||
fontSize: 16,
|
fontWeight: '300',
|
||||||
color: '#a855f7',
|
color: '#ffffff',
|
||||||
fontStyle: 'italic',
|
letterSpacing: 4,
|
||||||
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
formContainer: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
|
||||||
input: {
|
input: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
backgroundColor: '#1a1a2e',
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
fontSize: 16,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#ff6b6b',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
gradientButton: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
// iOS тени
|
// iOS тени
|
||||||
shadowColor: '#7c3aed',
|
shadowColor: '#ffffff',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 4,
|
height: 4,
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 6,
|
shadowRadius: 12,
|
||||||
// Android тень
|
// Android тень
|
||||||
elevation: 5,
|
elevation: 8,
|
||||||
},
|
},
|
||||||
button: {
|
disabledButton: {
|
||||||
marginTop: 20,
|
opacity: 0.5,
|
||||||
marginBottom: 10,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#9333ea',
|
|
||||||
// iOS тени для кнопки
|
|
||||||
shadowColor: '#9333ea',
|
|
||||||
shadowOffset: {
|
|
||||||
width: 0,
|
|
||||||
height: 8,
|
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.6,
|
buttonText: {
|
||||||
shadowRadius: 15,
|
color: '#000000',
|
||||||
// Android тень
|
fontSize: 16,
|
||||||
elevation: 10,
|
fontWeight: '700',
|
||||||
|
letterSpacing: 2,
|
||||||
},
|
},
|
||||||
buttonContent: {
|
dividerContainer: {
|
||||||
paddingVertical: 8,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: 24,
|
||||||
},
|
},
|
||||||
buttonLabel: {
|
divider: {
|
||||||
fontSize: 18,
|
flex: 1,
|
||||||
fontWeight: 'bold',
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
dividerText: {
|
||||||
|
color: '#666666',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
fontSize: 12,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
},
|
},
|
||||||
linkButton: {
|
linkButton: {
|
||||||
marginTop: 10,
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
linkButtonLabel: {
|
linkButtonText: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
color: '#a855f7',
|
color: '#808080',
|
||||||
},
|
},
|
||||||
errorText: {
|
linkButtonTextBold: {
|
||||||
color: '#ef4444',
|
fontSize: 14,
|
||||||
textAlign: 'center',
|
color: '#ffffff',
|
||||||
marginBottom: 10,
|
fontWeight: '600',
|
||||||
textShadowColor: '#ef4444',
|
|
||||||
textShadowOffset: { width: 0, height: 0 },
|
|
||||||
textShadowRadius: 5,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
@ -1,9 +1,10 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions } from 'react-native';
|
import { View, StyleSheet, KeyboardAvoidingView, Platform, ScrollView, Dimensions, TouchableOpacity } from 'react-native';
|
||||||
import { TextInput, Button, Text, Headline, HelperText } from 'react-native-paper';
|
import { TextInput, Button, Text, Headline, HelperText, Surface } from 'react-native-paper';
|
||||||
import { useMutation } from '@apollo/client';
|
import { useMutation } from '@apollo/client';
|
||||||
import { REGISTER } from '../graphql/mutations';
|
import { REGISTER } from '../graphql/mutations';
|
||||||
import { useAuth } from '../contexts/AuthContext';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { BackgroundDesign } from '../components/BackgroundDesign';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
@ -14,10 +15,13 @@ import Animated, {
|
|||||||
withRepeat,
|
withRepeat,
|
||||||
interpolate,
|
interpolate,
|
||||||
Easing,
|
Easing,
|
||||||
|
FadeIn,
|
||||||
|
FadeInDown,
|
||||||
|
FadeInUp,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window');
|
const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
|
||||||
const AnimatedView = Animated.View;
|
|
||||||
|
|
||||||
export const RegisterScreen = ({ navigation }: any) => {
|
export const RegisterScreen = ({ navigation }: any) => {
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@ -28,27 +32,21 @@ export const RegisterScreen = ({ navigation }: any) => {
|
|||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
|
||||||
// Анимации
|
// Анимации
|
||||||
const translateY = useSharedValue(50);
|
const cardScale = useSharedValue(0.95);
|
||||||
const opacity = useSharedValue(0);
|
const cardOpacity = useSharedValue(0);
|
||||||
const scale = useSharedValue(0.9);
|
|
||||||
const glowAnimation = useSharedValue(0);
|
const glowAnimation = useSharedValue(0);
|
||||||
const buttonScale = useSharedValue(1);
|
const buttonScale = useSharedValue(1);
|
||||||
const inputFocusAnimation1 = useSharedValue(0);
|
|
||||||
const inputFocusAnimation2 = useSharedValue(0);
|
|
||||||
const inputFocusAnimation3 = useSharedValue(0);
|
|
||||||
const inputFocusAnimation4 = useSharedValue(0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Анимация появления
|
// Анимация появления карточки
|
||||||
translateY.value = withSpring(0, { damping: 15, stiffness: 100 });
|
cardScale.value = withSpring(1, { damping: 15, stiffness: 100 });
|
||||||
opacity.value = withTiming(1, { duration: 800 });
|
cardOpacity.value = withTiming(1, { duration: 800 });
|
||||||
scale.value = withSpring(1, { damping: 15, stiffness: 100 });
|
|
||||||
|
|
||||||
// Пульсирующее свечение
|
// Мягкое свечение
|
||||||
glowAnimation.value = withRepeat(
|
glowAnimation.value = withRepeat(
|
||||||
withSequence(
|
withSequence(
|
||||||
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.ease) }),
|
withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.ease) }),
|
||||||
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.ease) })
|
withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.ease) })
|
||||||
),
|
),
|
||||||
-1,
|
-1,
|
||||||
false
|
false
|
||||||
@ -72,24 +70,20 @@ export const RegisterScreen = ({ navigation }: any) => {
|
|||||||
const passwordsMatch = password === confirmPassword || confirmPassword === '';
|
const passwordsMatch = password === confirmPassword || confirmPassword === '';
|
||||||
|
|
||||||
// Анимированные стили
|
// Анимированные стили
|
||||||
const containerAnimatedStyle = useAnimatedStyle(() => {
|
const cardAnimatedStyle = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
transform: [
|
transform: [{ scale: cardScale.value }],
|
||||||
{ translateY: translateY.value },
|
opacity: cardOpacity.value,
|
||||||
{ scale: scale.value }
|
|
||||||
],
|
|
||||||
opacity: opacity.value,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const glowContainerStyle = useAnimatedStyle(() => {
|
const glowStyle = useAnimatedStyle(() => {
|
||||||
const glowOpacity = interpolate(glowAnimation.value, [0, 1], [0.3, 0.8]);
|
const shadowOpacity = interpolate(glowAnimation.value, [0, 1], [0.1, 0.3]);
|
||||||
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [10, 30]);
|
const shadowRadius = interpolate(glowAnimation.value, [0, 1], [20, 40]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shadowOpacity: glowOpacity,
|
shadowOpacity: shadowOpacity,
|
||||||
shadowRadius: shadowRadius,
|
shadowRadius: shadowRadius,
|
||||||
elevation: interpolate(glowAnimation.value, [0, 1], [5, 15]),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -99,23 +93,7 @@ export const RegisterScreen = ({ navigation }: any) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const createInputAnimatedStyle = (focusAnimation: any) => {
|
|
||||||
return useAnimatedStyle(() => {
|
|
||||||
const borderWidth = interpolate(focusAnimation.value, [0, 1], [1, 2]);
|
|
||||||
const shadowOpacity = interpolate(focusAnimation.value, [0, 1], [0, 0.6]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
borderWidth: borderWidth,
|
|
||||||
shadowOpacity: shadowOpacity,
|
|
||||||
elevation: interpolate(focusAnimation.value, [0, 1], [2, 8]),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const inputStyle1 = createInputAnimatedStyle(inputFocusAnimation1);
|
|
||||||
const inputStyle2 = createInputAnimatedStyle(inputFocusAnimation2);
|
|
||||||
const inputStyle3 = createInputAnimatedStyle(inputFocusAnimation3);
|
|
||||||
const inputStyle4 = createInputAnimatedStyle(inputFocusAnimation4);
|
|
||||||
|
|
||||||
const handleButtonPressIn = () => {
|
const handleButtonPressIn = () => {
|
||||||
buttonScale.value = withSpring(0.95);
|
buttonScale.value = withSpring(0.95);
|
||||||
@ -126,136 +104,130 @@ export const RegisterScreen = ({ navigation }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<BackgroundDesign variant="login">
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
>
|
>
|
||||||
<ScrollView contentContainerStyle={styles.scrollContent}>
|
<ScrollView contentContainerStyle={styles.scrollContent}>
|
||||||
<Animated.View style={[styles.content, containerAnimatedStyle]}>
|
<Animated.View
|
||||||
<Animated.View style={[styles.glowContainer, glowContainerStyle]}>
|
style={[styles.content, cardAnimatedStyle]}
|
||||||
<Headline style={styles.title}>Регистрация в Prism</Headline>
|
entering={FadeInDown.duration(800).springify()}
|
||||||
<Text style={styles.subtitle}>Присоединяйтесь к будущему</Text>
|
>
|
||||||
|
<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>
|
</Animated.View>
|
||||||
|
|
||||||
<AnimatedView style={inputStyle1}>
|
<Animated.View
|
||||||
|
entering={FadeIn.delay(400).duration(600)}
|
||||||
|
style={styles.formContainer}
|
||||||
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Имя пользователя"
|
label="Имя пользователя"
|
||||||
value={username}
|
value={username}
|
||||||
onChangeText={setUsername}
|
onChangeText={setUsername}
|
||||||
mode="outlined"
|
mode="flat"
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
theme={{
|
theme={{
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#9333ea',
|
primary: '#ffffff',
|
||||||
placeholder: '#a855f7',
|
placeholder: '#808080',
|
||||||
text: '#ffffff',
|
text: '#ffffff',
|
||||||
background: '#1a1a2e',
|
background: 'rgba(255,255,255,0.05)',
|
||||||
outline: '#7c3aed',
|
outline: '#666666',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => {
|
underlineColor="transparent"
|
||||||
inputFocusAnimation1.value = withSpring(1);
|
activeUnderlineColor="#ffffff"
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
inputFocusAnimation1.value = withSpring(0);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</AnimatedView>
|
|
||||||
|
|
||||||
<AnimatedView style={inputStyle2}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Электронная почта"
|
||||||
value={email}
|
value={email}
|
||||||
onChangeText={setEmail}
|
onChangeText={setEmail}
|
||||||
mode="outlined"
|
mode="flat"
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
theme={{
|
theme={{
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#9333ea',
|
primary: '#ffffff',
|
||||||
placeholder: '#a855f7',
|
placeholder: '#808080',
|
||||||
text: '#ffffff',
|
text: '#ffffff',
|
||||||
background: '#1a1a2e',
|
background: 'rgba(255,255,255,0.05)',
|
||||||
outline: '#7c3aed',
|
outline: '#666666',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => {
|
underlineColor="transparent"
|
||||||
inputFocusAnimation2.value = withSpring(1);
|
activeUnderlineColor="#ffffff"
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
inputFocusAnimation2.value = withSpring(0);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</AnimatedView>
|
|
||||||
|
|
||||||
<AnimatedView style={inputStyle3}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Пароль"
|
label="Пароль"
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={setPassword}
|
onChangeText={setPassword}
|
||||||
mode="outlined"
|
mode="flat"
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
secureTextEntry={!showPassword}
|
secureTextEntry={!showPassword}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
theme={{
|
theme={{
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#9333ea',
|
primary: '#ffffff',
|
||||||
placeholder: '#a855f7',
|
placeholder: '#808080',
|
||||||
text: '#ffffff',
|
text: '#ffffff',
|
||||||
background: '#1a1a2e',
|
background: 'rgba(255,255,255,0.05)',
|
||||||
outline: '#7c3aed',
|
outline: '#666666',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
right={
|
right={
|
||||||
<TextInput.Icon
|
<TextInput.Icon
|
||||||
icon={showPassword ? 'eye-off' : 'eye'}
|
icon={showPassword ? 'eye-off' : 'eye'}
|
||||||
onPress={() => setShowPassword(!showPassword)}
|
onPress={() => setShowPassword(!showPassword)}
|
||||||
color="#a855f7"
|
color="#808080"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
onFocus={() => {
|
underlineColor="transparent"
|
||||||
inputFocusAnimation3.value = withSpring(1);
|
activeUnderlineColor="#ffffff"
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
inputFocusAnimation3.value = withSpring(0);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</AnimatedView>
|
|
||||||
|
|
||||||
<AnimatedView style={inputStyle4}>
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Подтвердите пароль"
|
label="Подтвердите пароль"
|
||||||
value={confirmPassword}
|
value={confirmPassword}
|
||||||
onChangeText={setConfirmPassword}
|
onChangeText={setConfirmPassword}
|
||||||
mode="outlined"
|
mode="flat"
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
secureTextEntry={!showPassword}
|
secureTextEntry={!showPassword}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
error={!passwordsMatch}
|
error={!passwordsMatch}
|
||||||
theme={{
|
theme={{
|
||||||
colors: {
|
colors: {
|
||||||
primary: '#9333ea',
|
primary: '#ffffff',
|
||||||
placeholder: '#a855f7',
|
placeholder: '#808080',
|
||||||
text: '#ffffff',
|
text: '#ffffff',
|
||||||
background: '#1a1a2e',
|
background: 'rgba(255,255,255,0.05)',
|
||||||
outline: '#7c3aed',
|
outline: passwordsMatch ? '#666666' : '#ff6b6b',
|
||||||
error: '#ef4444',
|
error: '#ff6b6b',
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onFocus={() => {
|
underlineColor="transparent"
|
||||||
inputFocusAnimation4.value = withSpring(1);
|
activeUnderlineColor="#ffffff"
|
||||||
}}
|
|
||||||
onBlur={() => {
|
|
||||||
inputFocusAnimation4.value = withSpring(0);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</AnimatedView>
|
|
||||||
|
|
||||||
{!passwordsMatch && (
|
{!passwordsMatch && confirmPassword !== '' && (
|
||||||
<HelperText type="error" visible={true} style={styles.errorText}>
|
<HelperText type="error" visible={true} style={styles.errorText}>
|
||||||
Пароли не совпадают
|
Пароли не совпадают
|
||||||
</HelperText>
|
</HelperText>
|
||||||
@ -267,51 +239,61 @@ export const RegisterScreen = ({ navigation }: any) => {
|
|||||||
</HelperText>
|
</HelperText>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<AnimatedView style={buttonAnimatedStyle}>
|
<Animated.View style={buttonAnimatedStyle}>
|
||||||
<Button
|
<TouchableOpacity
|
||||||
mode="contained"
|
|
||||||
onPress={handleRegister}
|
onPress={handleRegister}
|
||||||
onPressIn={handleButtonPressIn}
|
onPressIn={handleButtonPressIn}
|
||||||
onPressOut={handleButtonPressOut}
|
onPressOut={handleButtonPressOut}
|
||||||
loading={loading}
|
|
||||||
disabled={loading || !username || !email || !password || !passwordsMatch}
|
disabled={loading || !username || !email || !password || !passwordsMatch}
|
||||||
style={styles.button}
|
activeOpacity={0.8}
|
||||||
contentStyle={styles.buttonContent}
|
|
||||||
labelStyle={styles.buttonLabel}
|
|
||||||
theme={{
|
|
||||||
colors: {
|
|
||||||
primary: '#9333ea',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Зарегистрироваться
|
<LinearGradient
|
||||||
</Button>
|
colors={['#ffffff', '#e6e6e6']}
|
||||||
</AnimatedView>
|
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
|
<View style={styles.dividerContainer}>
|
||||||
mode="text"
|
<View style={styles.divider} />
|
||||||
|
<Text style={styles.dividerText}>ИЛИ</Text>
|
||||||
|
<View style={styles.divider} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
onPress={() => navigation.navigate('Login')}
|
onPress={() => navigation.navigate('Login')}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
style={styles.linkButton}
|
style={styles.linkButton}
|
||||||
labelStyle={styles.linkButtonLabel}
|
activeOpacity={0.7}
|
||||||
theme={{
|
|
||||||
colors: {
|
|
||||||
primary: '#a855f7',
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Уже есть аккаунт? Войти
|
<Text style={styles.linkButtonText}>
|
||||||
</Button>
|
Уже есть аккаунт?
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.linkButtonTextBold}>
|
||||||
|
{' Войти'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
</Animated.View>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
|
</BackgroundDesign>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#0a0a0f',
|
|
||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
@ -324,90 +306,117 @@ const styles = StyleSheet.create({
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
glowContainer: {
|
registerCard: {
|
||||||
marginBottom: 40,
|
borderRadius: 24,
|
||||||
padding: 20,
|
padding: 32,
|
||||||
borderRadius: 20,
|
backgroundColor: 'rgba(26, 26, 26, 0.8)',
|
||||||
backgroundColor: '#1a1a2e',
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
// backdropFilter: 'blur(10px)', // не поддерживается в React Native
|
||||||
// iOS тени
|
// iOS тени
|
||||||
shadowColor: '#9333ea',
|
shadowColor: '#ffffff',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.5,
|
shadowOpacity: 0.1,
|
||||||
shadowRadius: 20,
|
shadowRadius: 30,
|
||||||
// Android тень
|
// Android тень
|
||||||
elevation: 10,
|
elevation: 20,
|
||||||
|
},
|
||||||
|
gradientBackground: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderRadius: 24,
|
||||||
|
},
|
||||||
|
headerContainer: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 40,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
textAlign: 'center',
|
fontSize: 28,
|
||||||
marginBottom: 10,
|
fontWeight: '300',
|
||||||
fontSize: 32,
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
textShadowColor: '#9333ea',
|
letterSpacing: 3,
|
||||||
textShadowOffset: { width: 0, height: 0 },
|
marginBottom: 8,
|
||||||
textShadowRadius: 10,
|
|
||||||
},
|
},
|
||||||
subtitle: {
|
|
||||||
textAlign: 'center',
|
formContainer: {
|
||||||
fontSize: 16,
|
width: '100%',
|
||||||
color: '#a855f7',
|
|
||||||
fontStyle: 'italic',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
input: {
|
input: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
backgroundColor: '#1a1a2e',
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
fontSize: 16,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: '#ff6b6b',
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: 16,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
gradientButton: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
paddingHorizontal: 32,
|
||||||
|
borderRadius: 12,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: 8,
|
||||||
// iOS тени
|
// iOS тени
|
||||||
shadowColor: '#7c3aed',
|
shadowColor: '#ffffff',
|
||||||
shadowOffset: {
|
shadowOffset: {
|
||||||
width: 0,
|
width: 0,
|
||||||
height: 4,
|
height: 4,
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.3,
|
shadowOpacity: 0.3,
|
||||||
shadowRadius: 6,
|
shadowRadius: 12,
|
||||||
// Android тень
|
// Android тень
|
||||||
elevation: 5,
|
elevation: 8,
|
||||||
},
|
},
|
||||||
button: {
|
disabledButton: {
|
||||||
marginTop: 20,
|
opacity: 0.5,
|
||||||
marginBottom: 10,
|
|
||||||
borderRadius: 12,
|
|
||||||
backgroundColor: '#9333ea',
|
|
||||||
// iOS тени для кнопки
|
|
||||||
shadowColor: '#9333ea',
|
|
||||||
shadowOffset: {
|
|
||||||
width: 0,
|
|
||||||
height: 8,
|
|
||||||
},
|
},
|
||||||
shadowOpacity: 0.6,
|
buttonText: {
|
||||||
shadowRadius: 15,
|
color: '#000000',
|
||||||
// Android тень
|
fontSize: 16,
|
||||||
elevation: 10,
|
fontWeight: '700',
|
||||||
|
letterSpacing: 2,
|
||||||
},
|
},
|
||||||
buttonContent: {
|
dividerContainer: {
|
||||||
paddingVertical: 8,
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: 24,
|
||||||
},
|
},
|
||||||
buttonLabel: {
|
divider: {
|
||||||
fontSize: 18,
|
flex: 1,
|
||||||
fontWeight: 'bold',
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
},
|
||||||
|
dividerText: {
|
||||||
|
color: '#666666',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
fontSize: 12,
|
||||||
letterSpacing: 1,
|
letterSpacing: 1,
|
||||||
},
|
},
|
||||||
linkButton: {
|
linkButton: {
|
||||||
marginTop: 10,
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: 12,
|
||||||
},
|
},
|
||||||
linkButtonLabel: {
|
linkButtonText: {
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
color: '#a855f7',
|
color: '#808080',
|
||||||
},
|
},
|
||||||
errorText: {
|
linkButtonTextBold: {
|
||||||
color: '#ef4444',
|
fontSize: 14,
|
||||||
textAlign: 'center',
|
color: '#ffffff',
|
||||||
marginBottom: 10,
|
fontWeight: '600',
|
||||||
textShadowColor: '#ef4444',
|
|
||||||
textShadowOffset: { width: 0, height: 0 },
|
|
||||||
textShadowRadius: 5,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
Reference in New Issue
Block a user