├── .eslintrc ├── .expo-shared └── assets.json ├── .gitignore ├── .prettierrc ├── .watchmanconfig ├── App.js ├── LICENSE ├── README.md ├── app.json ├── assets ├── custom │ ├── icon.png │ └── splash.png ├── icon.png ├── images │ └── hello-world.jpg └── splash.png ├── babel.config.js ├── config.example.js ├── cover.png ├── final ├── Main.js ├── components │ ├── Loading.js │ ├── Note.js │ ├── NoteFeed.js │ └── UserForm.js └── screens │ ├── authloading.js │ ├── favorites.js │ ├── feed.js │ ├── index.js │ ├── mynotes.js │ ├── note.js │ ├── settings.js │ ├── signin.js │ └── signup.js ├── package-lock.json ├── package.json ├── solutions ├── 01-app-shell │ └── src │ │ ├── Main.js │ │ └── screens │ │ ├── favorites.js │ │ ├── feed.js │ │ ├── index.js │ │ ├── mynotes.js │ │ └── note.js ├── 02-graphql │ └── src │ │ ├── Main.js │ │ ├── components │ │ ├── Loading.js │ │ ├── Note.js │ │ └── NoteFeed.js │ │ └── screens │ │ ├── favorites.js │ │ ├── feed.js │ │ ├── index.js │ │ ├── mynotes.js │ │ └── note.js └── 03-authentication │ └── src │ ├── Main.js │ ├── components │ ├── Loading.js │ ├── Note.js │ ├── NoteFeed.js │ └── UserForm.js │ └── screens │ ├── authloading.js │ ├── favorites.js │ ├── feed.js │ ├── index.js │ ├── mynotes.js │ ├── note.js │ ├── settings.js │ ├── signin.js │ └── signup.js └── src └── Main.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:react/recommended", 5 | "prettier", 6 | "prettier/react" 7 | ], 8 | "parserOptions": { 9 | "ecmaVersion": 2017, 10 | "sourceType": "module", 11 | "ecmaFeatures": { 12 | "jsx": true 13 | } 14 | }, 15 | "plugins": ["react", "react-native", "prettier"], 16 | "env": { 17 | "es6": true, 18 | "browser": true, 19 | "node": true, 20 | "react-native/react-native": true 21 | }, 22 | "rules": { 23 | "no-console": "off", 24 | "no-unused-vars": "off" 25 | }, 26 | "settings": { 27 | "react": { 28 | "pragma": "React", 29 | "version": "detect" 30 | }, 31 | "react-native/style-sheet-object-names": [ 32 | "EStyleSheet", 33 | "OtherStyleSheet", 34 | "PStyleSheet" 35 | ] 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true, 4 | "c0e459814a661f9941b0946c7e2a25e780a52648d71f117621748eb09466a316": true, 5 | "423ed76c35119cbc680fcbdfe0bcaccb2bb087f0fc8e2841f2eed91a5bf19524": true, 6 | "8a5ce4677f57023947a31d23cad88731f76e6684576ef44f179a57be565f301f": true 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | config.js 13 | .vscode -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainApp from './src/Main'; 3 | 4 | export default function App() { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 JavaScript Everywhere 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # JavaScript Everywhere Mobile Application 4 | 5 | This repository contains code examples for the React Native mobile application chapters of [_JavaScript Everywhere_](https://www.jseverywhere.io/) by Adam D. Scott, published by O'Reilly Media 6 | 7 | ## Getting Help 8 | 9 | The best place to get help is our Spectrum channel, [spectrum.chat/jseverywhere](https://spectrum.chat/jseverywhere). 10 | 11 | ## Directory Structure 12 | 13 | - `/src` If you are following along with the book, this is the directory where you should perform your development. 14 | - `/solutions` This directory contains the solutions for each chapter. If you get stuck, these are available for you to consult. 15 | - `/final` This directory contains the final working project 16 | 17 | ## To Run the Application 18 | 19 | When developing locally, you can start the app by running: 20 | 21 | ``` 22 | npm start 23 | ``` 24 | 25 | ## Related Repositories 26 | 27 | - [API 🗄️ ](https://github.com/javascripteverywhere/api) 28 | - [Web 💻 ](https://github.com/javascripteverywhere/web) 29 | - [Desktop 🖥️](https://github.com/javascripteverywhere/desktop) 30 | 31 | ## Code of Conduct 32 | 33 | In the interest of fostering an open and welcoming environment, I pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.. 34 | 35 | This project pledges to follow the [Contributor's Covenant](http://contributor-covenant.org/version/1/4/). 36 | 37 | ## Photo Attribution 38 | 39 | This project includes the photo "Hello World," by [Wendell Oskay](https://www.flickr.com/photos/oskay/472097903), licensed under [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/). 40 | 41 | ## License 42 | 43 | Copyright 2019 Adam D. Scott 44 | 45 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 48 | 49 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Notedly", 4 | "slug": "notedly-mobile", 5 | "description": "An example React Native app", 6 | "privacy": "public", 7 | "version": "1.0.0", 8 | "platforms": ["ios", "android"], 9 | "orientation": "portrait", 10 | "icon": "./assets/custom/icon.png", 11 | "splash": { 12 | "image": "./assets/custom/splash.png", 13 | "resizeMode": "cover", 14 | "backgroundColor": "#4A90E2" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 1500 18 | }, 19 | "assetBundlePatterns": ["**/*"], 20 | "ios": { 21 | "supportsTablet": true 22 | }, 23 | "android": { 24 | "adaptiveIcon": { 25 | "foregroundImage": "./assets/custom/icon.png", 26 | "backgroundColor": "#4A90E2" 27 | } 28 | }, 29 | "web": { 30 | "favicon": "./assets/favicon.png" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /assets/custom/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/mobile/bef7e9e4f1da9d642f35b779af150c87277d5801/assets/custom/icon.png -------------------------------------------------------------------------------- /assets/custom/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/mobile/bef7e9e4f1da9d642f35b779af150c87277d5801/assets/custom/splash.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/mobile/bef7e9e4f1da9d642f35b779af150c87277d5801/assets/icon.png -------------------------------------------------------------------------------- /assets/images/hello-world.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/mobile/bef7e9e4f1da9d642f35b779af150c87277d5801/assets/images/hello-world.jpg -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/mobile/bef7e9e4f1da9d642f35b779af150c87277d5801/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /config.example.js: -------------------------------------------------------------------------------- 1 | // credit to Alex Martinez & Peter Piekarczyk for Environment configuration inspiration 2 | // https://medium.com/@peterpme/environment-variables-in-expo-using-release-channels-4934594c5307 3 | // https://alxmrtnz.com/thoughts/2019/03/12/environment-variables-and-workflow-in-expo.html 4 | import Constants from 'expo-constants'; 5 | 6 | // get the localhost ip address at runtime using the Expo manifest 7 | // this enables both simulator and physical device debugging with our local api 8 | let localhost; 9 | if (Constants.manifest.debuggerHost) { 10 | localhost = Constants.manifest.debuggerHost.split(':').shift(); 11 | } 12 | 13 | // set environment variables 14 | const ENV = { 15 | dev: { 16 | API_URI: `http://${localhost}:4000/api` 17 | }, 18 | prod: { 19 | // update the API_URI value with your publicly deployed API address 20 | API_URI: 'https://' 21 | } 22 | }; 23 | 24 | const getEnvVars = (env = Constants.manifest.releaseChannel) => { 25 | // __DEV__ is set to true when react-native is running locally in dev mode 26 | // __DEV__ is set to false when our app is published 27 | if (__DEV__) { 28 | return ENV.dev; 29 | } else if (env === 'prod') { 30 | return ENV.prod; 31 | } 32 | }; 33 | 34 | export default getEnvVars; 35 | -------------------------------------------------------------------------------- /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/javascripteverywhere/mobile/bef7e9e4f1da9d642f35b779af150c87277d5801/cover.png -------------------------------------------------------------------------------- /final/Main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Screens from './screens'; 3 | // import the Apollo libraries 4 | import { 5 | ApolloClient, 6 | ApolloProvider, 7 | createHttpLink, 8 | InMemoryCache 9 | } from '@apollo/client'; 10 | import { setContext } from 'apollo-link-context'; 11 | // import SecureStore for retrieving the token value 12 | import * as SecureStore from 'expo-secure-store'; 13 | 14 | // import environment configuration 15 | import getEnvVars from '../config'; 16 | const { API_URI } = getEnvVars(); 17 | 18 | // configure our API URI & cache 19 | const uri = API_URI; 20 | const cache = new InMemoryCache(); 21 | const httpLink = createHttpLink({ uri }); 22 | 23 | // return the headers to the context 24 | const authLink = setContext(async (_, { headers }) => { 25 | return { 26 | headers: { 27 | ...headers, 28 | authorization: (await SecureStore.getItemAsync('token')) || '' 29 | } 30 | }; 31 | }); 32 | 33 | // configure Apollo Client 34 | const client = new ApolloClient({ 35 | link: authLink.concat(httpLink), 36 | cache 37 | }); 38 | 39 | const Main = () => { 40 | // wrap our app in the ApolloProvider higher-order component 41 | return ( 42 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default Main; 49 | -------------------------------------------------------------------------------- /final/components/Loading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, ActivityIndicator } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | 5 | const LoadingWrap = styled.View` 6 | flex: 1; 7 | justify-content: center; 8 | align-items: center; 9 | `; 10 | 11 | const Loading = () => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default Loading; 20 | -------------------------------------------------------------------------------- /final/components/Note.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, ScrollView } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | import Markdown from 'react-native-markdown-renderer'; 5 | import { format } from 'date-fns'; 6 | 7 | const NoteView = styled.ScrollView` 8 | padding: 10px; 9 | `; 10 | 11 | const NoteComponent = ({ note }) => { 12 | return ( 13 | 14 | 15 | Note by {note.author.username} / Published{' '} 16 | {format(new Date(note.createdAt), 'MMM do yyyy')} 17 | 18 | {note.content} 19 | 20 | ); 21 | }; 22 | 23 | export default NoteComponent; 24 | -------------------------------------------------------------------------------- /final/components/NoteFeed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FlatList, View, TouchableOpacity, Text } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | 5 | import NoteComponent from './Note'; 6 | 7 | // our dummy data 8 | // const notes = [ 9 | // { id: 0, content: 'Giant Steps' }, 10 | // { id: 1, content: 'Tomorrow Is The Question' }, 11 | // { id: 2, content: 'Tonight At Noon' }, 12 | // { id: 3, content: 'Out To Lunch' }, 13 | // { id: 4, content: 'Green Street' }, 14 | // { id: 5, content: 'In A Silent Way' }, 15 | // { id: 6, content: 'Lanquidity' }, 16 | // { id: 7, content: 'Nuff Said' }, 17 | // { id: 8, content: 'Nova' }, 18 | // { id: 9, content: 'The Awakening' } 19 | // ]; 20 | 21 | // FeedView styled-component definition 22 | const FeedView = styled.View` 23 | height: 100; 24 | overflow: hidden; 25 | margin-bottom: 10px; 26 | `; 27 | 28 | const Separator = styled.View` 29 | height: 1; 30 | width: 100%; 31 | background-color: #ced0ce; 32 | `; 33 | 34 | const NoteFeed = props => { 35 | return ( 36 | 37 | item.id.toString()} 40 | ItemSeparatorComponent={() => } 41 | renderItem={({ item }) => ( 42 | 44 | props.navigation.navigate('Note', { 45 | id: item.id 46 | }) 47 | } 48 | > 49 | 50 | 51 | 52 | 53 | )} 54 | /> 55 | 56 | ); 57 | }; 58 | 59 | export default NoteFeed; 60 | -------------------------------------------------------------------------------- /final/components/UserForm.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { View, Text, TextInput, Button, TouchableOpacity } from 'react-native'; 3 | import styled from 'styled-components/native'; 4 | 5 | const FormView = styled.View` 6 | padding: 10px; 7 | `; 8 | 9 | const StyledInput = styled.TextInput` 10 | border: 1px solid gray; 11 | font-size: 18px; 12 | padding: 8px; 13 | margin-bottom: 24px; 14 | `; 15 | 16 | const FormLabel = styled.Text` 17 | font-size: 18px; 18 | font-weight: bold; 19 | `; 20 | 21 | const FormButton = styled.TouchableOpacity` 22 | background: #0077cc; 23 | width: 100%; 24 | padding: 8px; 25 | `; 26 | 27 | const ButtonText = styled.Text` 28 | text-align: center; 29 | color: #fff; 30 | font-weight: bold; 31 | font-size: 18px; 32 | `; 33 | 34 | const SignUp = styled.TouchableOpacity` 35 | margin-top: 20px; 36 | `; 37 | 38 | const Link = styled.Text` 39 | color: #0077cc; 40 | font-weight: bold; 41 | `; 42 | 43 | const UserForm = props => { 44 | const [email, setEmail] = useState(); 45 | const [password, setPassword] = useState(); 46 | const [username, setUsername] = useState(); 47 | 48 | const handleSubmit = () => { 49 | props.action({ 50 | variables: { 51 | email: email, 52 | password: password, 53 | username: username 54 | } 55 | }); 56 | }; 57 | 58 | return ( 59 | 60 | Email 61 | setEmail(text)} 63 | value={email} 64 | textContentType="emailAddress" 65 | autoCompleteType="email" 66 | autoFocus={true} 67 | autoCapitalize="none" 68 | /> 69 | {props.formType === 'signUp' && ( 70 | 71 | Username 72 | setUsername(text)} 74 | value={username} 75 | textContentType="username" 76 | autoCapitalize="none" 77 | /> 78 | 79 | )} 80 | Password 81 | setPassword(text)} 83 | value={password} 84 | textContentType="password" 85 | secureTextEntry={true} 86 | /> 87 | 88 | Submit 89 | 90 | {props.formType !== 'signUp' && ( 91 | props.navigation.navigate('SignUp')}> 92 | 93 | Need an account? Sign up. 94 | 95 | 96 | )} 97 | 98 | ); 99 | }; 100 | 101 | export default UserForm; 102 | -------------------------------------------------------------------------------- /final/screens/authloading.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import * as SecureStore from 'expo-secure-store'; 3 | 4 | import Loading from '../components/Loading'; 5 | 6 | const AuthLoading = props => { 7 | const checkLoginState = async () => { 8 | // retrieve the value of the token 9 | const userToken = await SecureStore.getItemAsync('token'); 10 | // navigate to the app screen if a token is present 11 | // else navigate to the auth screen 12 | props.navigation.navigate(userToken ? 'App' : 'Auth'); 13 | }; 14 | 15 | // call checkLoginState as soon as the component mounts 16 | useEffect(() => { 17 | checkLoginState(); 18 | }); 19 | 20 | return ; 21 | }; 22 | 23 | export default AuthLoading; 24 | -------------------------------------------------------------------------------- /final/screens/favorites.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { useQuery, gql } from '@apollo/client'; 4 | 5 | import NoteFeed from '../components/NoteFeed'; 6 | import Loading from '../components/Loading'; 7 | 8 | // our GraphQL query 9 | const GET_MY_FAVORITES = gql` 10 | query me { 11 | me { 12 | id 13 | username 14 | favorites { 15 | id 16 | createdAt 17 | content 18 | favoriteCount 19 | author { 20 | username 21 | id 22 | avatar 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | 29 | const Favorites = props => { 30 | const { loading, error, data } = useQuery(GET_MY_FAVORITES); 31 | 32 | // if the data is loading, our app will display a loading message 33 | if (loading) return ; 34 | // if there is an error fetching the data, display an error message 35 | if (error) return Error loading notes; 36 | // if the query is successful and there are notes, return the feed of notes 37 | // else if the query is successful and there aren't notes, display a message 38 | if (data.me.favorites.length !== 0) { 39 | return ; 40 | } else { 41 | return No notes yet; 42 | } 43 | }; 44 | 45 | Favorites.navigationOptions = { 46 | title: 'Favorites' 47 | }; 48 | 49 | export default Favorites; 50 | -------------------------------------------------------------------------------- /final/screens/feed.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text } from 'react-native'; 3 | // import our Apollo libraries 4 | import { useQuery, gql } from '@apollo/client'; 5 | 6 | import NoteFeed from '../components/NoteFeed'; 7 | import Loading from '../components/Loading'; 8 | 9 | // compose our query 10 | const GET_NOTES = gql` 11 | query notes { 12 | notes { 13 | id 14 | createdAt 15 | content 16 | favoriteCount 17 | author { 18 | username 19 | id 20 | avatar 21 | } 22 | } 23 | } 24 | `; 25 | 26 | const Feed = props => { 27 | const { loading, error, data } = useQuery(GET_NOTES); 28 | 29 | // if the data is loading, our app will display a loading indicator 30 | if (loading) return ; 31 | // if there is an error fetching the data, display an error message 32 | if (error) return Error loading notes; 33 | // if the query is successful and there are notes, return the feed of notes 34 | return ; 35 | }; 36 | 37 | Feed.navigationOptions = { 38 | title: 'Feed' 39 | }; 40 | 41 | export default Feed; 42 | -------------------------------------------------------------------------------- /final/screens/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View, ScrollView, Button } from 'react-native'; 3 | import { createAppContainer, createSwitchNavigator } from 'react-navigation'; 4 | import { createBottomTabNavigator } from 'react-navigation-tabs'; 5 | import { createStackNavigator } from 'react-navigation-stack'; 6 | import { MaterialCommunityIcons } from '@expo/vector-icons'; 7 | 8 | // import screen components 9 | import Feed from './feed'; 10 | import Favorites from './favorites'; 11 | import MyNotes from './mynotes'; 12 | import NoteScreen from './note'; 13 | import AuthLoading from './authloading'; 14 | import SignIn from './signin'; 15 | import SignUp from './signup'; 16 | import Settings from './settings'; 17 | 18 | const AuthStack = createStackNavigator({ 19 | SignIn: SignIn, 20 | SignUp: SignUp 21 | }); 22 | 23 | const SettingsStack = createStackNavigator({ 24 | Settings: Settings 25 | }); 26 | 27 | const FeedStack = createStackNavigator({ 28 | Feed: Feed, 29 | Note: NoteScreen 30 | }); 31 | 32 | const MyStack = createStackNavigator({ 33 | MyNotes: MyNotes, 34 | Note: NoteScreen 35 | }); 36 | 37 | const FavStack = createStackNavigator({ 38 | Favorites: Favorites, 39 | Note: NoteScreen 40 | }); 41 | 42 | const TabNavigator = createBottomTabNavigator({ 43 | FeedScreen: { 44 | screen: FeedStack, 45 | navigationOptions: { 46 | tabBarLabel: 'Feed', 47 | tabBarIcon: ({ tintColor }) => ( 48 | 49 | ) 50 | } 51 | }, 52 | MyNoteScreen: { 53 | screen: MyStack, 54 | navigationOptions: { 55 | tabBarLabel: 'My Notes', 56 | tabBarIcon: ({ tintColor }) => ( 57 | 58 | ) 59 | } 60 | }, 61 | FavoriteScreen: { 62 | screen: FavStack, 63 | navigationOptions: { 64 | tabBarLabel: 'Favorites', 65 | tabBarIcon: ({ tintColor }) => ( 66 | 67 | ) 68 | } 69 | }, 70 | Settings: { 71 | screen: SettingsStack, 72 | navigationOptions: { 73 | tabBarLabel: 'Settings', 74 | tabBarIcon: ({ tintColor }) => ( 75 | 76 | ) 77 | } 78 | } 79 | }); 80 | 81 | const SwitchNavigator = createSwitchNavigator( 82 | { 83 | AuthLoading: AuthLoading, 84 | Auth: AuthStack, 85 | App: TabNavigator 86 | }, 87 | { 88 | initialRouteName: 'AuthLoading' 89 | } 90 | ); 91 | 92 | export default createAppContainer(SwitchNavigator); 93 | -------------------------------------------------------------------------------- /final/screens/mynotes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { useQuery, gql } from '@apollo/client'; 4 | 5 | import NoteFeed from '../components/NoteFeed'; 6 | import Loading from '../components/Loading'; 7 | 8 | // our GraphQL query 9 | const GET_MY_NOTES = gql` 10 | query me { 11 | me { 12 | id 13 | username 14 | notes { 15 | id 16 | createdAt 17 | content 18 | favoriteCount 19 | author { 20 | username 21 | id 22 | avatar 23 | } 24 | } 25 | } 26 | } 27 | `; 28 | 29 | const MyNotes = props => { 30 | const { loading, error, data } = useQuery(GET_MY_NOTES); 31 | 32 | // if the data is loading, our app will display a loading message 33 | if (loading) return ; 34 | // if there is an error fetching the data, display an error message 35 | if (error) return Error loading notes; 36 | // if the query is successful and there are notes, return the feed of notes 37 | // else if the query is successful and there aren't notes, display a message 38 | if (data.me.notes.length !== 0) { 39 | return ; 40 | } else { 41 | return No notes yet; 42 | } 43 | }; 44 | 45 | MyNotes.navigationOptions = { 46 | title: 'My Notes' 47 | }; 48 | 49 | export default MyNotes; 50 | -------------------------------------------------------------------------------- /final/screens/note.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { useQuery, gql } from '@apollo/client'; 4 | 5 | import Note from '../components/Note'; 6 | import Loading from '../components/Loading'; 7 | 8 | // our note query, which accepts an ID variable 9 | const GET_NOTE = gql` 10 | query note($id: ID!) { 11 | note(id: $id) { 12 | id 13 | createdAt 14 | content 15 | favoriteCount 16 | author { 17 | username 18 | id 19 | avatar 20 | } 21 | } 22 | } 23 | `; 24 | 25 | const NoteScreen = props => { 26 | const id = props.navigation.getParam('id'); 27 | const { loading, error, data } = useQuery(GET_NOTE, { variables: { id } }); 28 | 29 | if (loading) return ; 30 | // if there's an error, display this message to the user 31 | if (error) return Error! Note not found; 32 | // if successful, pass the data to the note component 33 | return ; 34 | }; 35 | 36 | export default NoteScreen; 37 | -------------------------------------------------------------------------------- /final/screens/settings.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Button } from 'react-native'; 3 | import * as SecureStore from 'expo-secure-store'; 4 | 5 | const Settings = props => { 6 | const signOut = () => { 7 | SecureStore.deleteItemAsync('token').then( 8 | props.navigation.navigate('Auth') 9 | ); 10 | }; 11 | 12 | return ( 13 | 14 |