diff --git a/package.json b/package.json index 7ac7f4f..f9853b6 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "react": "17.0.1", "react-i18next": "^11.8.4", "react-native": "0.63.4", + "react-native-app-intro-slider": "^4.0.4", "react-native-circular-progress": "^1.3.7", "react-native-dotenv": "^2.5.3", "react-native-dropdown-picker": "^5.1.21", diff --git a/src/images/mockPhone.svg b/src/images/mockPhone.svg new file mode 100644 index 0000000..b775456 --- /dev/null +++ b/src/images/mockPhone.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/mockPhoneWithGeo.svg b/src/images/mockPhoneWithGeo.svg new file mode 100644 index 0000000..3446cd6 --- /dev/null +++ b/src/images/mockPhoneWithGeo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/images/onboardingBottom.png b/src/images/onboardingBottom.png new file mode 100644 index 0000000..6ae4982 Binary files /dev/null and b/src/images/onboardingBottom.png differ diff --git a/src/images/onboardingHighBottom.png b/src/images/onboardingHighBottom.png new file mode 100644 index 0000000..e52bffa Binary files /dev/null and b/src/images/onboardingHighBottom.png differ diff --git a/src/images/onboardingHighTop.png b/src/images/onboardingHighTop.png new file mode 100644 index 0000000..410419c Binary files /dev/null and b/src/images/onboardingHighTop.png differ diff --git a/src/images/onboardingTop.png b/src/images/onboardingTop.png new file mode 100644 index 0000000..bee1042 Binary files /dev/null and b/src/images/onboardingTop.png differ diff --git a/src/navigation/mainTabs.tsx b/src/navigation/mainTabs.tsx index 7afe40b..6b11b8d 100644 --- a/src/navigation/mainTabs.tsx +++ b/src/navigation/mainTabs.tsx @@ -11,6 +11,7 @@ import Account from '../images/navigation/account.svg'; import Quests from '../images/navigation/quests.svg'; import TabBar from '../components/TabBar'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useAuthContext } from '../contexts/AuthProvider'; /** * Type with params of screens and their props in BottomTabNavigator @@ -36,6 +37,7 @@ const Icon = styled.View<{color: string}>` * Functional component for implementing navigation between screens */ export default function MainTabsNavigation(): React.ReactElement { + const authContext = useAuthContext(); const { t } = useTranslation(); const insets = useSafeAreaInsets(); @@ -76,6 +78,7 @@ export default function MainTabsNavigation(): React.ReactElement { { return ; diff --git a/src/navigation/profileStack.tsx b/src/navigation/profileStack.tsx index 4d73a0b..1fef4c0 100644 --- a/src/navigation/profileStack.tsx +++ b/src/navigation/profileStack.tsx @@ -15,6 +15,7 @@ import FriendAddingScreenWithSuspense from '../screens/FriendsAdding'; import ChangeUsernameScreen from '../screens/ChangeUsername'; import ChangePasswordScreen from '../screens/ChangePassword'; import AchievementsScreenWithSuspense from '../screens/Achievements'; +import OnboardingSlider from '../onboarding/OnboardingSlider'; /** * Type with params of screens and their props in ProfileStackScreen @@ -94,6 +95,11 @@ export type ProfileStackParamList = { * FriendAdding screen props */ FriendAdding: undefined; + + /** + * Onboarding stack props + */ + Onboarding: undefined; }; const ProfileStack = createStackNavigator(); @@ -105,8 +111,13 @@ export default function ProfileStackNavigation(): React.ReactElement { const authContext = useAuthContext(); return ( - - {authContext.state.isFirstRegistration && } + + {authContext.state.isFirstRegistration && + <> + + + + } diff --git a/src/onboarding/OnboardingSlider.tsx b/src/onboarding/OnboardingSlider.tsx new file mode 100644 index 0000000..4762d11 --- /dev/null +++ b/src/onboarding/OnboardingSlider.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import AppIntroSlider from 'react-native-app-intro-slider'; +import MainInfo from './screens/MainInfo'; +import AboutQuests from './screens/AboutQuests'; +import AboutGeolocation from './screens/AboutGeolocation'; +import QuestPassing from './screens/QuestPassing'; +import AboutModalize from './screens/AboutModalize'; +import Colors from '../styles/colors'; +import styled from 'styled-components/native'; +import { StyledFonts } from '../styles/textStyles'; +import { useAuthContext } from '../contexts/AuthProvider'; +import Back from '../images/back.svg'; + +const data = [ + { + index: 1, + component: , + }, + { + index: 2, + component: , + }, + { + index: 3, + component: , + }, + { + index: 4, + component: , + }, + { + index: 5, + component: , + }, +]; + +type Item = typeof data[0]; + +const dot = { + backgroundColor: 'rgba(104, 198, 223, 0.3)', + width: 15, + height: 15, + borderRadius: 8, +}; + +const activeDot = { + backgroundColor: Colors.Blue, + width: 15, + height: 15, + borderRadius: 8, +}; + +const DoneButton = styled.TouchableOpacity` + padding: 13px; + border-radius: 30px; + flex-direction: row; + align-items: center; +`; + +const ButtonText = styled.Text` + ${StyledFonts.uiWebMedium}; + font-size: 16px; + color: ${Colors.DarkBlue}; +`; + +const NextArrow = styled(Back)` + color: ${Colors.DarkBlue}; + transform: rotate(180deg); + margin-left: 10px; +`; + +/** + * + */ +export default function OnboardingSlider(): React.ReactElement { + const authContext = useAuthContext(); + + const _renderItem = ({ item }: {item: Item}): React.ReactElement => { + return ( + <> + {item.component} + + ); + }; + + const _renderDoneButton = (): React.ReactElement => { + return ( + authContext.actions.setFirstRegistrationFalse()}> + Done + + + ); + }; + + const _keyExtractor = (item: Item): string => item.index.toString(); + + return ( + + ); +} diff --git a/src/onboarding/components/OnboardingBody.tsx b/src/onboarding/components/OnboardingBody.tsx new file mode 100644 index 0000000..f566049 --- /dev/null +++ b/src/onboarding/components/OnboardingBody.tsx @@ -0,0 +1,61 @@ +import React from 'react'; +import styled from 'styled-components/native'; +import Colors from '../../styles/colors'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { Dimensions } from 'react-native'; + +const Body = styled.View<{bottomOffset: number}>` + background-color: ${Colors.Background}; + flex: 1; + padding: ${props => 60 + props.bottomOffset}px 15px; + justify-content: space-between; +`; + +const PatternView = styled.View<{pos: 'top' | 'bottom'}>` + position: absolute; + ${props => props.pos === 'top' ? 'top: 0;' : 'bottom: 0;'} + width: ${Dimensions.get('screen').width}px; + aspect-ratio: ${375 / 218}; +`; + +const Pattern = styled.Image` + width: 100%; + height: 100%; +`; + +interface OnboardingBodyProps { + /** + * If it is first screen of onboarding + */ + isFirstScreen?: boolean, + + /** + * Children + */ + children: React.ReactElement[] | React.ReactElement, +} + +/** + * Wrapper for onboarding screens + * + * @param props - props for component rendering + */ +export default function OnboardingBody({ isFirstScreen, children }: OnboardingBodyProps): React.ReactElement { + const insets = useSafeAreaInsets(); + + return ( + + + + + {children} + + + + + ); +} diff --git a/src/onboarding/components/QuestType.tsx b/src/onboarding/components/QuestType.tsx new file mode 100644 index 0000000..e3e5963 --- /dev/null +++ b/src/onboarding/components/QuestType.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import styled from 'styled-components/native'; +import Colors from '../../styles/colors'; +import { StyledFonts } from '../../styles/textStyles'; + +const Row = styled.View` + flex-direction: row; + align-self: stretch; + align-items: center; +`; + +const IconView = styled.View` + background-color: ${Colors.Blue}; + width: 60px; + aspect-ratio: 1; + border-radius: 30px; + align-items: center; + justify-content: center; + margin-right: 15px; +`; + +const TextView = styled.View` + flex: 1; +`; + +const Title = styled.Text` + ${StyledFonts.uiWebMedium}; + font-size: 22px; + line-height: 22px; + color: ${Colors.Black}; +`; + +const Description = styled.Text` + ${StyledFonts.uiWebRegular}; + font-size: 16px; + line-height: 19px; + color: ${Colors.Black}; +`; + +interface QuestTypeProps { + /** + * Icon of current type + */ + icon: React.ReactElement, + + /** + * Name of current type + */ + title: string, + + /** + * Information about current type + */ + description: string, +} + +/** + * Block with information about specific type of quests + * + * @param props - props for component rendering + */ +export default function QuestType({ icon, title, description }: QuestTypeProps): React.ReactElement { + return ( + + {icon} + + {title} + {description} + + + ); +} diff --git a/src/onboarding/components/ScreenInfo.tsx b/src/onboarding/components/ScreenInfo.tsx new file mode 100644 index 0000000..c3b76af --- /dev/null +++ b/src/onboarding/components/ScreenInfo.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { StyledFonts } from '../../styles/textStyles'; +import Colors from '../../styles/colors'; +import styled from 'styled-components/native'; +import { View } from 'react-native'; + +const Title = styled.Text` + ${StyledFonts.roboto}; + font-size: 28px; + line-height: 28px; + color: ${Colors.Black}; + margin: 20px 0 15px; + align-self: stretch; +`; + +const Description = styled.Text` + ${StyledFonts.uiWebRegular}; + font-size: 22px; + line-height: 22px; + color: ${Colors.Black}; + align-self: stretch; +`; + +interface ScreenInfoProps { + /** + * Title + */ + title: string, + + /** + * Information + */ + description: string, +} + +/** + * Block with main information on screen + * + * @param props - props for component rendering + */ +export default function ScreenInfo({ title, description }: ScreenInfoProps): React.ReactElement { + return ( + + {title} + {description} + + ); +} diff --git a/src/onboarding/screens/AboutGeolocation.tsx b/src/onboarding/screens/AboutGeolocation.tsx new file mode 100644 index 0000000..eb74f40 --- /dev/null +++ b/src/onboarding/screens/AboutGeolocation.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import OnboardingBody from '../components/OnboardingBody'; +import MockPhone from '../../images/mockPhoneWithGeo.svg'; +import { Dimensions } from 'react-native'; +import ScreenInfo from '../components/ScreenInfo'; +import styled from 'styled-components/native'; + +const MockPhoneView = styled.View` + flex: 1; + max-height: ${Dimensions.get('screen'). width * 400 / 375 + 50}px; + margin-bottom: 20px; + align-items: center; + justify-content: flex-end; +`; + +/** + * Screen with information about geolocation + */ +export default function AboutGeolocation(): React.ReactElement { + const mockPhoneWidth = Dimensions.get('screen').width; + const [mockPhoneHeight, setMockPhoneHeight] = useState(mockPhoneWidth * 400 / 375); + + return ( + + event.nativeEvent.layout.height > 0 && setMockPhoneHeight(Math.min(mockPhoneHeight, event.nativeEvent.layout.height))}> + + + + + ); +} diff --git a/src/onboarding/screens/AboutModalize.tsx b/src/onboarding/screens/AboutModalize.tsx new file mode 100644 index 0000000..73eb6c1 --- /dev/null +++ b/src/onboarding/screens/AboutModalize.tsx @@ -0,0 +1,131 @@ +import React, { useState } from 'react'; +import OnboardingBody from '../components/OnboardingBody'; +import MockPhone from '../../images/mockPhone.svg'; +import ScreenInfo from '../components/ScreenInfo'; +import styled from 'styled-components/native'; +import { Dimensions, View } from 'react-native'; +import Colors from '../../styles/colors'; +import LinearGradient from 'react-native-linear-gradient'; +import TextBlock from '../../components/questBlocks/Text/TextBlock'; +import { PageBlock } from '../../types/questData'; +import { TargetLocationProvider } from '../../contexts/TargetLocationContext'; +import { AudioAccompanimentProvider } from '../../contexts/AudioAccompanimentContext'; + +const pageBlock: PageBlock = { + 'type': 'page', + 'data': [ + { + 'type': 'header', + 'data': { + 'level': 2, + 'text': 'Есенщина', + }, + }, + { + 'type': 'paragraph', + 'data': { + 'text': 'Причина образования слова — противостояние Есенина и Маяковского. Маяковский глубоко осуждал Есенина, в частности за его меланхоличные мысли:', + }, + }, + { + 'type': 'quote', + 'data': { + 'caption': 'Выступление на диспуте «Упадочное настроение среди молодежи (Есенщина)», 5 марта 1927 г., публикация Ф. Н. Пицкельэ ', + 'text': '«По вопросу об есенинщине я глубоко убежден, конечно, что Есенин сам по себе не так страшен и не мог бы быть так страшен, как есенинщина. Конечно, есенинщина производное от Есенина, потому что многие идеализируют в этом отношении Есенина, а он не имеет никакого отношения к этому. Но на множество процентов это результат дальнейших популяризаторов, дальнейших пропагандистов Есенина. Нельзя же все-таки скрывать такой факт, что выступавшие товарищи, и т. Бухарин в своих заметках выступали не только против есенинщины, а против Есенина, против Есенина самого, как он есть.»', + }, + }, + ], +}; + +const MockPhoneView = styled.View` + flex: 1; + max-height: ${Dimensions.get('screen'). width * 400 / 375 + 50}px; + margin-bottom: 20px; + align-items: center; + justify-content: flex-end; +`; + +const GradientView = styled.View` + height: 147%; + width: 100%; + position: absolute; + align-self: center; + z-index: 999; + elevation: ${999}; +`; + +const Gradient = styled(LinearGradient)` + flex: 1; +`; + +const ModalizeContainer = styled.View<{width: number, height: number}>` + background-color: ${Colors.White}; + height: ${props => props.height}px; + width: ${props => props.width}px; + position: absolute; + bottom: 0; + align-self: center; + border-radius: 10px; + elevation: ${16}; + box-shadow: 0 8px 10px rgba(0, 0, 0, 0.2); +`; + +const ModalizeView = styled.View` + flex: 1; + padding: 12px 15px; + overflow: hidden; +`; + +const Handle = styled.View` + background-color: rgba(85, 85, 107, 0.1); + width: 50px; + height: 5px; + border-radius: 3px; + align-self: center; + margin-bottom: 20px; +`; + +/** + * + */ +export default function AboutModalize(): React.ReactElement { + const [mockPhoneHeight, setMockPhoneHeight] = useState(Dimensions.get('screen').width * 400 / 375); + const mockPhoneWidth = mockPhoneHeight * 375 / 400; + const modalizeWidth = mockPhoneWidth * 305 / 375; + const modalizeHeight = mockPhoneHeight * 325 / 400; + + return ( + + + + event.nativeEvent.layout.height > 0 && setMockPhoneHeight(Math.min(mockPhoneHeight, event.nativeEvent.layout.height))}> + + + + + + console.log()}/> + + + + + + + + + + + + ); +} diff --git a/src/onboarding/screens/AboutQuests.tsx b/src/onboarding/screens/AboutQuests.tsx new file mode 100644 index 0000000..4db697f --- /dev/null +++ b/src/onboarding/screens/AboutQuests.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import OnboardingBody from '../components/OnboardingBody'; +import Human from '../../images/human.svg'; +import Route from '../../images/route.svg'; +import Book from '../../images/book.svg'; +import Puzzle from '../../images/puzzle.svg'; +import QuestType from '../components/QuestType'; +import ScreenInfo from '../components/ScreenInfo'; +import styled from 'styled-components/native'; +import { Dimensions } from 'react-native'; + +const Container = styled.View` + flex: 1; + max-height: ${Dimensions.get('screen'). width * 400 / 375 + 50}px; + margin-bottom: 40px; + justify-content: flex-end; +`; + +const QuestTypesView = styled.View` + flex: 1; + max-height: 400px; + justify-content: space-between; +`; + +/** + * Screen with information about quests + */ +export default function AboutQuests(): React.ReactElement { + return ( + + + + } title={'Квесты'} description={'Прогулки по городу с заданиями и интерактивными действиями'}/> + } title={'Маршруты'} description={'Подборка связанных между собой локаций по одной теме'}/> + } title={'Истории'} description={'Интерактивные новеллы. Выходить из дома необязательно :)'}/> + } title={'Тесты'} description={'Проверь свое знание истории и культуры города'}/> + + + + + ); +} diff --git a/src/onboarding/screens/MainInfo.tsx b/src/onboarding/screens/MainInfo.tsx new file mode 100644 index 0000000..9d42293 --- /dev/null +++ b/src/onboarding/screens/MainInfo.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import OnboardingBody from '../components/OnboardingBody'; +import LogoIcon from '../../images/fullLogo.svg'; +import styled from 'styled-components/native'; +import { StyledFonts } from '../../styles/textStyles'; +import Colors from '../../styles/colors'; + +const Container = styled.View` + flex: 1; + align-items: center; + justify-content: center; +`; + +const Logo = styled(LogoIcon)` + margin-bottom: 25px; +`; + +const DefaultText = styled.Text` + ${StyledFonts.uiWebMedium}; + line-height: 22px; + font-size: 22px; + color: ${Colors.Black}; + text-align: center; +`; + +const Delimiter = styled.View` + background-color: ${Colors.Blue}; + height: 1px; + margin: 25px 75px; + align-self: stretch; +`; + +/** + * First screen of onboarding + */ +export default function MainInfo(): React.ReactElement { + return ( + + + + Que.St: квесты по Петербургу + + Приложение, посвещенное изучению истории и культруы Санкт-Петербурга! + + + ); +} diff --git a/src/onboarding/screens/QuestPassing.tsx b/src/onboarding/screens/QuestPassing.tsx new file mode 100644 index 0000000..d058781 --- /dev/null +++ b/src/onboarding/screens/QuestPassing.tsx @@ -0,0 +1,67 @@ +import React, { useState } from 'react'; +import OnboardingBody from '../components/OnboardingBody'; +import MockPhone from '../../images/mockPhone.svg'; +import ScreenInfo from '../components/ScreenInfo'; +import { Dimensions, Platform, View } from 'react-native'; +import styled from 'styled-components/native'; +import Colors from '../../styles/colors'; +import Alarm from '../../images/alarm.svg'; +import { StyledFonts } from '../../styles/textStyles'; + +const MockPhoneView = styled.View` + flex: 1; + max-height: ${Dimensions.get('screen'). width * 400 / 375 + 50}px; + margin-bottom: 20px; + align-items: center; + justify-content: flex-end; +`; + +const TaskView = styled.View<{width: number, topOffset: number}>` + min-height: 64px; + width: ${props => props.width}px; + position: absolute; + top: ${props => props.topOffset}px; + align-self: center; + background-color: ${Colors.White}; + ${Platform.OS === 'ios' && 'border: rgba(85, 85, 107, 0.15) 1px;'} + border-radius: 10px; + padding: 15px 20px; + flex-direction: row; + align-items: center; + elevation: ${16}; + box-shadow: 0 8px 10px rgba(0, 0, 0, 0.2); +`; + +const TaskText = styled.Text` + ${StyledFonts.uiWebRegular}; + flex: 1; + margin-left: 15px; + font-size: 14px; + line-height: 16px; + color: ${Colors.Black}; +`; + +/** + * Screen with information about passing the quest + */ +export default function QuestPassing(): React.ReactElement { + const [mockPhoneHeight, setMockPhoneHeight] = useState(Dimensions.get('screen').width * 400 / 375); + const mockPhoneWidth = mockPhoneHeight * 375 / 400; + const taskViewWidth = mockPhoneWidth * 320 / 375; + const taskViewTopOffset = mockPhoneHeight / 10; + + return ( + + event.nativeEvent.layout.height > 0 && setMockPhoneHeight(Math.min(mockPhoneHeight, event.nativeEvent.layout.height))}> + + + + + Добраться до квартиры Бриков, где жила Лиля Брик + + + + + + ); +} diff --git a/src/screens/ChangeUsername.tsx b/src/screens/ChangeUsername.tsx index 5fb0755..af6b0c4 100644 --- a/src/screens/ChangeUsername.tsx +++ b/src/screens/ChangeUsername.tsx @@ -12,7 +12,13 @@ import { graphql } from 'react-relay'; import { useMutation } from 'react-relay/hooks'; import Tip from '../images/tip.svg'; import { ChangeUsernameMutation } from './__generated__/ChangeUsernameMutation.graphql'; -import { useAuthContext } from '../contexts/AuthProvider'; +import { ProfileStackParamList } from '../navigation/profileStack'; +import { StackScreenProps } from '@react-navigation/stack'; + +/** + * Type with props of screen 'ChangeUsername' in ProfileStackScreen + */ +type Props = StackScreenProps; /** * Styles for login view @@ -65,10 +71,11 @@ const StyledButton = styled(Button)` /** * Screen with changing username + * + * @param props - props for component rendering */ -export default function ChangeUsernameScreen(): ReactElement { +export default function ChangeUsernameScreen({ navigation }: Props): ReactElement { const { t } = useTranslation(); - const authContext = useAuthContext(); const [username, setUsername] = useState(''); const [ updateUsername ] = useMutation( @@ -105,14 +112,16 @@ export default function ChangeUsernameScreen(): ReactElement { /> updateUsername({ - variables: { username }, - onError: error => Alert.alert(t([`errors.${error.source.errors[0].extensions.code}`, 'errors.unspecific'])), - onCompleted: () => { - Alert.alert(t('signUp.successful')); - authContext.actions.setFirstRegistrationFalse(); - }, - })} + onPress={() => { + updateUsername({ + variables: { username }, + onError: error => Alert.alert(t([`errors.${error.source.errors[0].extensions.code}`, 'errors.unspecific'])), + onCompleted: () => { + Alert.alert(t('signUp.successful')); + navigation.navigate('Onboarding'); + }, + }); + }} /> ); diff --git a/yarn.lock b/yarn.lock index 55e3428..e1b4ea0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7683,6 +7683,11 @@ react-is@^16.12.0, react-is@^16.13.0, react-is@^16.7.0, react-is@^16.8.1, react- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-native-app-intro-slider@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/react-native-app-intro-slider/-/react-native-app-intro-slider-4.0.4.tgz#fa5cda7057db62c448ac975ffd2ba0cff94cc8d8" + integrity sha512-Zkjaol6X3BbZkHUpVDj2LjdidpS6rCgKi0fx80xgGKa0pHxBRd4swWTv2bHnnvu5k1/HXwYk0mY2TbK+2jHl5w== + react-native-circular-progress@^1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/react-native-circular-progress/-/react-native-circular-progress-1.3.7.tgz#cc430fbc612bd01134a8fc9667be107591ae6959"