├── .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 |
15 |
16 | );
17 | };
18 |
19 | Settings.navigationOptions = {
20 | title: 'Settings'
21 | };
22 |
23 | export default Settings;
24 |
--------------------------------------------------------------------------------
/final/screens/signin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Button, Text } from 'react-native';
3 | import * as SecureStore from 'expo-secure-store';
4 | import { useMutation, gql } from '@apollo/client';
5 |
6 | import UserForm from '../components/UserForm';
7 | import Loading from '../components/Loading';
8 |
9 | const SIGNIN_USER = gql`
10 | mutation signIn($email: String, $password: String!) {
11 | signIn(email: $email, password: $password)
12 | }
13 | `;
14 |
15 | const SignIn = props => {
16 | const [signIn, { loading, error }] = useMutation(SIGNIN_USER, {
17 | onCompleted: data => {
18 | // store the token with a key value of `token`
19 | // after the token is stored navigate to the app's main screen
20 | SecureStore.setItemAsync('token', data.signIn).then(
21 | props.navigation.navigate('App')
22 | );
23 | }
24 | });
25 |
26 | // if loading, return a loading indicator
27 | if (loading) return ;
28 | return (
29 |
30 | {error && Error signing in!}
31 |
36 |
37 | );
38 | };
39 |
40 | SignIn.navigationOptions = {
41 | title: 'Sign In'
42 | };
43 |
44 | export default SignIn;
45 |
--------------------------------------------------------------------------------
/final/screens/signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Button, Text } from 'react-native';
3 | import * as SecureStore from 'expo-secure-store';
4 | import { useMutation, gql } from '@apollo/client';
5 |
6 | import UserForm from '../components/UserForm';
7 | import Loading from '../components/Loading';
8 |
9 | const SIGNUP_USER = gql`
10 | mutation signUp($email: String!, $username: String!, $password: String!) {
11 | signUp(email: $email, username: $username, password: $password)
12 | }
13 | `;
14 |
15 | const SignUp = props => {
16 | const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
17 | onCompleted: data => {
18 | // store the token with a key value of `token`
19 | // after the token is stored navigate to the app's main screen
20 | SecureStore.setItemAsync('token', data.signUp).then(
21 | props.navigation.navigate('App')
22 | );
23 | }
24 | });
25 |
26 | // if loading, return a loading indicator
27 | if (loading) return ;
28 |
29 | return (
30 |
31 | {error && Error signing in!}
32 |
37 |
38 | );
39 | };
40 |
41 | SignUp.navigationOptions = {
42 | title: 'Register'
43 | };
44 |
45 | export default SignUp;
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "node_modules/expo/AppEntry.js",
3 | "scripts": {
4 | "start": "expo start",
5 | "android": "expo start --android",
6 | "ios": "expo start --ios",
7 | "web": "expo start --web",
8 | "eject": "expo eject"
9 | },
10 | "dependencies": {
11 | "@apollo/client": "^3.2.7",
12 | "@react-native-community/masked-view": "0.1.10",
13 | "apollo-link-context": "^1.0.20",
14 | "date-fns": "^2.16.1",
15 | "expo": "~39.0.2",
16 | "expo-constants": "^9.2.0",
17 | "expo-secure-store": "^9.2.0",
18 | "expo-status-bar": "~1.0.2",
19 | "graphql": "^15.4.0",
20 | "react": "16.13.1",
21 | "react-apollo": "^3.1.5",
22 | "react-dom": "16.13.1",
23 | "react-native": "https://github.com/expo/react-native/archive/sdk-39.0.4.tar.gz",
24 | "react-native-gesture-handler": "~1.7.0",
25 | "react-native-markdown-renderer": "^3.2.8",
26 | "react-native-reanimated": "~1.13.0",
27 | "react-native-safe-area-context": "3.1.4",
28 | "react-native-screens": "~2.10.1",
29 | "react-native-tab-view": "2.14.2",
30 | "react-native-web": "~0.13.12",
31 | "react-navigation": "^4.4.3",
32 | "react-navigation-stack": "^2.10.1",
33 | "react-navigation-tabs": "^2.10.1",
34 | "styled-components": "^5.2.1"
35 | },
36 | "devDependencies": {
37 | "@babel/core": "~7.9.0",
38 | "babel-preset-expo": "^8.3.0",
39 | "eslint": "^7.13.0",
40 | "eslint-config-prettier": "^6.15.0",
41 | "eslint-plugin-react-native": "^3.10.0",
42 | "prettier": "^2.1.2"
43 | },
44 | "private": true
45 | }
46 |
--------------------------------------------------------------------------------
/solutions/01-app-shell/src/Main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Screens from './screens';
3 |
4 | const Main = () => {
5 | return ;
6 | };
7 |
8 | export default Main;
9 |
--------------------------------------------------------------------------------
/solutions/01-app-shell/src/screens/favorites.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | const Favorites = () => {
5 | return (
6 |
7 | Favorites
8 |
9 | );
10 | };
11 |
12 | Favorites.navigationOptions = {
13 | title: 'Favorites'
14 | };
15 |
16 | export default Favorites;
17 |
--------------------------------------------------------------------------------
/solutions/01-app-shell/src/screens/feed.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View, Button } from 'react-native';
3 |
4 | const Feed = props => {
5 | return (
6 |
7 | Note Feed
8 |
13 | );
14 | };
15 |
16 | Feed.navigationOptions = {
17 | title: 'Feed'
18 | };
19 |
20 | export default Feed;
21 |
--------------------------------------------------------------------------------
/solutions/01-app-shell/src/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 Feed from './feed';
9 | import Favorites from './favorites';
10 | import MyNotes from './mynotes';
11 | import NoteScreen from './note';
12 |
13 | const FeedStack = createStackNavigator({
14 | Feed: Feed,
15 | Note: NoteScreen
16 | });
17 |
18 | const MyStack = createStackNavigator({
19 | MyNotes: MyNotes,
20 | Note: NoteScreen
21 | });
22 |
23 | const FavStack = createStackNavigator({
24 | Favorites: Favorites,
25 | Note: NoteScreen
26 | });
27 |
28 | const TabNavigator = createBottomTabNavigator({
29 | FeedScreen: {
30 | screen: FeedStack,
31 | navigationOptions: {
32 | tabBarLabel: 'Feed',
33 | tabBarIcon: ({ tintColor }) => (
34 |
35 | )
36 | }
37 | },
38 | MyNoteScreen: {
39 | screen: MyStack,
40 | navigationOptions: {
41 | tabBarLabel: 'My Notes',
42 | tabBarIcon: ({ tintColor }) => (
43 |
44 | )
45 | }
46 | },
47 | FavoriteScreen: {
48 | screen: FavStack,
49 | navigationOptions: {
50 | tabBarLabel: 'Favorites',
51 | tabBarIcon: ({ tintColor }) => (
52 |
53 | )
54 | }
55 | }
56 | });
57 |
58 | export default createAppContainer(TabNavigator);
59 |
--------------------------------------------------------------------------------
/solutions/01-app-shell/src/screens/mynotes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | const MyNotes = () => {
5 | return (
6 |
7 | My Notes
8 |
9 | );
10 | };
11 |
12 | MyNotes.navigationOptions = {
13 | title: 'My Notes'
14 | };
15 |
16 | export default MyNotes;
17 |
--------------------------------------------------------------------------------
/solutions/01-app-shell/src/screens/note.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | const NoteScreen = () => {
5 | return (
6 |
7 | This is a note!
8 |
9 | );
10 | };
11 |
12 | export default NoteScreen;
13 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/Main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Screens from './screens';
3 | // import the Apollo libraries
4 | import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
5 | // import environment configuration
6 | import getEnvVars from '../config';
7 | const { API_URI } = getEnvVars();
8 |
9 | // configure our API URI & cache
10 | const uri = API_URI;
11 | const cache = new InMemoryCache();
12 |
13 | // configure Apollo Client
14 | const client = new ApolloClient({
15 | uri,
16 | cache
17 | });
18 |
19 | const Main = () => {
20 | // wrap our app in the ApolloProvider higher-order component
21 | return (
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | export default Main;
29 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/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 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/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 Note = ({ 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 Note;
24 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/components/NoteFeed.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FlatList, View, TouchableOpacity } 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 | 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 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/screens/favorites.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | const Favorites = () => {
5 | return (
6 |
7 | Favorites
8 |
9 | );
10 | };
11 |
12 | Favorites.navigationOptions = {
13 | title: 'Favorites'
14 | };
15 |
16 | export default Favorites;
17 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/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 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/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 Feed from './feed';
9 | import Favorites from './favorites';
10 | import MyNotes from './mynotes';
11 | import NoteScreen from './note';
12 |
13 | const FeedStack = createStackNavigator({
14 | Feed: Feed,
15 | Note: NoteScreen
16 | });
17 |
18 | const MyStack = createStackNavigator({
19 | MyNotes: MyNotes,
20 | Note: NoteScreen
21 | });
22 |
23 | const FavStack = createStackNavigator({
24 | Favorites: Favorites,
25 | Note: NoteScreen
26 | });
27 |
28 | const TabNavigator = createBottomTabNavigator({
29 | FeedScreen: {
30 | screen: FeedStack,
31 | navigationOptions: {
32 | tabBarLabel: 'Feed',
33 | tabBarIcon: ({ tintColor }) => (
34 |
35 | )
36 | }
37 | },
38 | MyNoteScreen: {
39 | screen: MyStack,
40 | navigationOptions: {
41 | tabBarLabel: 'My Notes',
42 | tabBarIcon: ({ tintColor }) => (
43 |
44 | )
45 | }
46 | },
47 | FavoriteScreen: {
48 | screen: FavStack,
49 | navigationOptions: {
50 | tabBarLabel: 'Favorites',
51 | tabBarIcon: ({ tintColor }) => (
52 |
53 | )
54 | }
55 | }
56 | });
57 |
58 | export default createAppContainer(TabNavigator);
59 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/screens/mynotes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | const MyNotes = () => {
5 | return (
6 |
7 | My Notes
8 |
9 | );
10 | };
11 |
12 | MyNotes.navigationOptions = {
13 | title: 'My Notes'
14 | };
15 |
16 | export default MyNotes;
17 |
--------------------------------------------------------------------------------
/solutions/02-graphql/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 Feed from './feed';
9 | import Favorites from './favorites';
10 | import MyNotes from './mynotes';
11 | import NoteScreen from './note';
12 | import AuthLoading from './authloading';
13 | import SignIn from './signin';
14 | import SignUp from './signup';
15 | import Settings from './settings';
16 |
17 | const AuthStack = createStackNavigator({
18 | SignIn: SignIn,
19 | SignUp: SignUp
20 | });
21 |
22 | const SettingsStack = createStackNavigator({
23 | Settings: Settings
24 | });
25 |
26 | const FeedStack = createStackNavigator({
27 | Feed: Feed,
28 | Note: NoteScreen
29 | });
30 |
31 | const MyStack = createStackNavigator({
32 | MyNotes: MyNotes,
33 | Note: NoteScreen
34 | });
35 |
36 | const FavStack = createStackNavigator({
37 | Favorites: Favorites,
38 | Note: NoteScreen
39 | });
40 |
41 | const TabNavigator = createBottomTabNavigator({
42 | FeedScreen: {
43 | screen: FeedStack,
44 | navigationOptions: {
45 | tabBarLabel: 'Feed',
46 | tabBarIcon: ({ tintColor }) => (
47 |
48 | )
49 | }
50 | },
51 | MyNoteScreen: {
52 | screen: MyStack,
53 | navigationOptions: {
54 | tabBarLabel: 'My Notes',
55 | tabBarIcon: ({ tintColor }) => (
56 |
57 | )
58 | }
59 | },
60 | FavoriteScreen: {
61 | screen: FavStack,
62 | navigationOptions: {
63 | tabBarLabel: 'Favorites',
64 | tabBarIcon: ({ tintColor }) => (
65 |
66 | )
67 | }
68 | },
69 | Settings: {
70 | screen: SettingsStack,
71 | navigationOptions: {
72 | tabBarLabel: 'Settings',
73 | tabBarIcon: ({ tintColor }) => (
74 |
75 | )
76 | }
77 | }
78 | });
79 |
80 | const SwitchNavigator = createSwitchNavigator(
81 | {
82 | AuthLoading: AuthLoading,
83 | Auth: AuthStack,
84 | App: TabNavigator
85 | },
86 | {
87 | initialRouteName: 'AuthLoading'
88 | }
89 | );
90 |
91 | export default createAppContainer(SwitchNavigator);
92 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/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 |
15 |
16 | );
17 | };
18 |
19 | Settings.navigationOptions = {
20 | title: 'Settings'
21 | };
22 |
23 | export default Settings;
24 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/screens/signin.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Button, Text } from 'react-native';
3 | import * as SecureStore from 'expo-secure-store';
4 | import { useMutation, gql } from '@apollo/client';
5 |
6 | import UserForm from '../components/UserForm';
7 | import Loading from '../components/Loading';
8 |
9 | const SIGNIN_USER = gql`
10 | mutation signIn($email: String, $password: String!) {
11 | signIn(email: $email, password: $password)
12 | }
13 | `;
14 |
15 | const SignIn = props => {
16 | const [signIn, { loading, error }] = useMutation(SIGNIN_USER, {
17 | onCompleted: data => {
18 | // store the token with a key value of `token`
19 | // after the token is stored navigate to the app's main screen
20 | SecureStore.setItemAsync('token', data.signIn).then(
21 | props.navigation.navigate('App')
22 | );
23 | }
24 | });
25 |
26 | // if loading, return a loading indicator
27 | if (loading) return ;
28 | return (
29 |
30 | {error && Error signing in!}
31 |
36 |
37 | );
38 | };
39 |
40 | SignIn.navigationOptions = {
41 | title: 'Sign In'
42 | };
43 |
44 | export default SignIn;
45 |
--------------------------------------------------------------------------------
/solutions/03-authentication/src/screens/signup.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { View, Button, Text } from 'react-native';
3 | import * as SecureStore from 'expo-secure-store';
4 | import { useMutation, gql } from '@apollo/client';
5 |
6 | import UserForm from '../components/UserForm';
7 | import Loading from '../components/Loading';
8 |
9 | const SIGNUP_USER = gql`
10 | mutation signUp($email: String!, $username: String!, $password: String!) {
11 | signUp(email: $email, username: $username, password: $password)
12 | }
13 | `;
14 |
15 | const SignUp = props => {
16 | const [signUp, { loading, error }] = useMutation(SIGNUP_USER, {
17 | onCompleted: data => {
18 | // store the token with a key value of `token`
19 | // after the token is stored navigate to the app's main screen
20 | SecureStore.setItemAsync('token', data.signUp).then(
21 | props.navigation.navigate('App')
22 | );
23 | }
24 | });
25 |
26 | // if loading, return a loading indicator
27 | if (loading) return ;
28 |
29 | return (
30 |
31 | {error && Error signing in!}
32 |
37 |
38 | );
39 | };
40 |
41 | SignUp.navigationOptions = {
42 | title: 'Register'
43 | };
44 |
45 | export default SignUp;
46 |
--------------------------------------------------------------------------------
/src/Main.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Text, View } from 'react-native';
3 |
4 | const Main = () => {
5 | return (
6 |
7 | Hello world!
8 |
9 | );
10 | };
11 |
12 | export default Main;
13 |
--------------------------------------------------------------------------------