├── .eslintrc ├── .gitignore ├── .watchmanconfig ├── App.js ├── LICENSE ├── README.md ├── ReactotronConfig.js ├── app.json ├── assets ├── icon.png └── splash.png ├── babel.config.js ├── components ├── Header.js ├── MyStatusBar.js └── ProductCard.js ├── navigation ├── DrawerNavigation.js └── RootNavigation.js ├── package.json ├── screens ├── FeedScreen.js ├── LoginScreen.js └── ProductScreen.js └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "settings": { 11 | "import/resolver": { 12 | "node": { 13 | "extensions": [".js", ".ios.js", ".android.js"] 14 | } 15 | } 16 | }, 17 | "globals": { 18 | "__DEV__": false, 19 | "GLOBAL": false 20 | }, 21 | "rules": { 22 | "arrow-parens": ["error", "always"], 23 | "function-paren-newline": ["error", "consistent"], 24 | "no-confusing-arrow": ["off"], 25 | "no-multiple-empty-lines": ["error", { "max": 1, "maxEOF": 0, "maxBOF": 0 }], 26 | "no-underscore-dangle": ["off"], 27 | "object-curly-newline": ["error", { "consistent": true }], 28 | "prefer-promise-reject-errors": ["off"], 29 | "import/no-named-default": ["off"], 30 | "import/prefer-default-export": ["off"], 31 | "jsx-a11y/anchor-is-valid": ["off"], 32 | "jsx-a11y/click-events-have-key-events": ["off"], 33 | "jsx-a11y/label-has-for": [ "error", { "required": { "every": ["id"] }, "allowChildren": true } ], 34 | "jsx-a11y/no-noninteractive-element-interactions": ["off"], 35 | "jsx-a11y/no-static-element-interactions": ["off"], 36 | "react/jsx-filename-extension": ["error", { "extensions": [".js"] }], 37 | "no-use-before-define": ["error", { "variables": false }], 38 | "react/prop-types": 0, 39 | "no-alert": 0, 40 | "no-console": 0 41 | } 42 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.log 5 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, SafeAreaView } from 'react-native'; 3 | import MyStatusBar from './components/MyStatusBar'; 4 | import RootNavigation from './navigation/RootNavigation'; 5 | 6 | if (__DEV__) { 7 | import('./ReactotronConfig').then(() => console.log('Reactotron Configured')); 8 | } 9 | 10 | const styles = StyleSheet.create({ 11 | container: { 12 | flex: 1, 13 | backgroundColor: '#ffffff', 14 | }, 15 | }); 16 | 17 | // const showApiCalls = () => { 18 | // const baseUrl = 'http://www.mocky.io/'; 19 | // global._fetch = fetch; 20 | // global.fetch = async (uri, options, ...args) => { 21 | // const response = await global._fetch(uri, options, ...args); 22 | // if (uri.includes(baseUrl)) { 23 | // console.log( 24 | // '🔵 API Call: ', 25 | // uri, 26 | // { request: { uri }, response }, 27 | // ); 28 | // } 29 | // return response; 30 | // }; 31 | // }; 32 | 33 | class App extends React.Component { 34 | constructor(props) { 35 | super(props); 36 | 37 | if (__DEV__) { 38 | console.disableYellowBox = true; 39 | // showApiCalls(); 40 | } 41 | } 42 | 43 | render() { 44 | return ( 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | } 52 | 53 | export default App; 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kashish Grover 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 | # React Native Workshop 2 | 3 | Problem Statement: 4 | 5 | To create a simple example for a React Native e-commerce app. 6 | 7 | 1. Create a simple login screen with a hard coded id and password 8 | 2. Create a generic list screen using the following api link: [http://www.mocky.io/v2/5b35cb7c2f0000692d3763c5] 9 | 3. On clicking a particular product, navigate to a screen where you display all other details 10 | 11 | Concepts covered: 12 | 13 | 1. Setting up a project 14 | 2. RN Components (View, Text, Image, ScrollView, KeyboardAvoidingView, etc.) 15 | 3. Styling 16 | 4. AsyncStorage 17 | 5. Linting Setup 18 | 6. React Navigation - Stack and Drawer Navigators 19 | 7. Passing props while navigating 20 | 8. Using internal state 21 | 9. Project structure -------------------------------------------------------------------------------- /ReactotronConfig.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | import Reactotron from 'reactotron-react-native'; 3 | 4 | Reactotron.configure().useReactNative().connect(); 5 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "RNShop", 4 | "description": "A rather simple boilerplate with some alleged React Native best practices. ", 5 | "slug": "react-native-workshop", 6 | "privacy": "public", 7 | "sdkVersion": "32.0.0", 8 | "platforms": [ 9 | "ios", 10 | "android" 11 | ], 12 | "version": "0.0.1", 13 | "orientation": "portrait", 14 | "icon": "./assets/icon.png", 15 | "splash": { 16 | "image": "./assets/splash.png", 17 | "resizeMode": "contain", 18 | "backgroundColor": "#ffffff" 19 | }, 20 | "updates": { 21 | "fallbackToCacheTimeout": 0 22 | }, 23 | "assetBundlePatterns": [ 24 | "**/*" 25 | ], 26 | "ios": { 27 | "supportsTablet": true 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactBlr/react-native-workshop/82b432567150c99dfa43835f4d01b0f53ce9763c/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactBlr/react-native-workshop/82b432567150c99dfa43835f4d01b0f53ce9763c/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | module.exports = function(api) { 3 | api.cache(true); 4 | return { 5 | presets: ['babel-preset-expo'], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /components/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; 3 | import Ionicons from 'react-native-vector-icons/Ionicons'; 4 | import { withNavigation } from 'react-navigation'; 5 | 6 | const Header = ({ navigation, title, subtitle }) => ( 7 | 8 | navigation.goBack()}> 9 | 10 | 11 | 12 | {!!title && ( 13 | 14 | {title} 15 | 16 | )} 17 | {!!subtitle && ( 18 | 19 | {subtitle} 20 | 21 | )} 22 | 23 | 24 | ); 25 | 26 | export default withNavigation(Header); 27 | 28 | const styles = StyleSheet.create({ 29 | header: { 30 | backgroundColor: 'white', 31 | flexDirection: 'row', 32 | alignItems: 'center', 33 | height: 44, 34 | }, 35 | backButton: { 36 | paddingHorizontal: 16, 37 | paddingVertical: 8, 38 | left: 0, 39 | }, 40 | title: { 41 | fontWeight: '500', 42 | fontSize: 16, 43 | }, 44 | subTitle: { 45 | fontWeight: '400', 46 | fontSize: 12, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /components/MyStatusBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatusBar, View, Platform } from 'react-native'; 3 | 4 | const MyStatusBar = () => ( 5 | 6 | 11 | {Platform.OS === 'android' && Platform.Version >= 20 && ( 12 | 18 | )} 19 | 20 | ); 21 | 22 | export default MyStatusBar; 23 | -------------------------------------------------------------------------------- /components/ProductCard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TouchableOpacity, StyleSheet, Image, View } from 'react-native'; 3 | 4 | class ProductCard extends React.Component { 5 | state = {}; 6 | 7 | render() { 8 | const { navigation, product } = this.props; 9 | 10 | const isProductAvailable = product.availability === 'in stock'; 11 | 12 | return ( 13 | navigation.navigate('Product', product)} 15 | activeOpacity={0.8} 16 | style={styles.card} 17 | > 18 | 26 | 27 | 28 | {product.title} 29 | 30 | {isProductAvailable 31 | ? ( 32 | 33 | {`₹ ${product.price}`} 34 | 35 | ) 36 | : ( 37 | 38 | Sold Out 39 | 40 | ) 41 | } 42 | 43 | 44 | ); 45 | } 46 | } 47 | 48 | export default ProductCard; 49 | 50 | const styles = StyleSheet.create({ 51 | card: { 52 | backgroundColor: 'white', 53 | marginTop: 16, 54 | borderRadius: 2, 55 | elevation: 1, 56 | flexDirection: 'row', 57 | padding: 8, 58 | }, 59 | image: { 60 | height: 120, 61 | width: 120, 62 | }, 63 | title: { 64 | fontWeight: '500', 65 | fontSize: 18, 66 | }, 67 | price: { 68 | paddingTop: 8, 69 | fontSize: 16, 70 | }, 71 | soldOutText: { 72 | paddingTop: 8, 73 | color: '#fc6c85', 74 | fontWeight: '500', 75 | }, 76 | }); 77 | -------------------------------------------------------------------------------- /navigation/DrawerNavigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createBottomTabNavigator } from 'react-navigation'; 3 | import Ionicons from 'react-native-vector-icons/Ionicons'; 4 | 5 | import FeedScreen from '../screens/FeedScreen'; 6 | import LoginScreen from '../screens/LoginScreen'; 7 | 8 | export default createBottomTabNavigator( 9 | { 10 | Home: { 11 | screen: FeedScreen, 12 | }, 13 | Profile: { 14 | screen: LoginScreen, 15 | }, 16 | }, 17 | { 18 | navigationOptions: ({ navigation }) => ({ 19 | tabBarIcon: ({ tintColor }) => { 20 | const { routeName } = navigation.state; 21 | let iconName; 22 | if (routeName === 'Home') { 23 | iconName = 'ios-home'; 24 | } else if (routeName === 'Profile') { 25 | iconName = 'ios-person'; 26 | } 27 | 28 | return ; 29 | }, 30 | }), 31 | tabBarOptions: { 32 | activeTintColor: 'purple', 33 | inactiveTintColor: 'gray', 34 | }, 35 | }, 36 | ); 37 | -------------------------------------------------------------------------------- /navigation/RootNavigation.js: -------------------------------------------------------------------------------- 1 | import { createStackNavigator } from 'react-navigation'; 2 | 3 | import DrawerNavigator from './DrawerNavigation'; 4 | import ProductScreen from '../screens/ProductScreen'; 5 | 6 | export default createStackNavigator( 7 | { 8 | Home: { 9 | screen: DrawerNavigator, 10 | }, 11 | Product: { 12 | screen: ProductScreen, 13 | }, 14 | }, 15 | { 16 | navigationOptions: () => ({ 17 | header: null, 18 | }), 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /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 | "eject": "expo eject" 8 | }, 9 | "private": true, 10 | "dependencies": { 11 | "expo": "^32.0.0", 12 | "lodash": "^4.17.10", 13 | "react": "16.5.0", 14 | "react-native": "https://github.com/expo/react-native/archive/sdk-32.0.0.tar.gz", 15 | "react-native-vector-icons": "^4.6.0", 16 | "react-navigation": "^2.5.5" 17 | }, 18 | "devDependencies": { 19 | "babel-eslint": "^8.2.5", 20 | "babel-preset-expo": "^5.0.0", 21 | "eslint": "^5.0.1", 22 | "eslint-config-airbnb": "^17.0.0", 23 | "eslint-plugin-import": "^2.13.0", 24 | "eslint-plugin-jsx-a11y": "^6.0.3", 25 | "eslint-plugin-react": "^7.10.0", 26 | "reactotron-react-native": "^2.1.5" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /screens/FeedScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ActivityIndicator, FlatList, StyleSheet, View, Text, Image } from 'react-native'; 3 | import ProductCard from '../components/ProductCard'; 4 | 5 | const GET_DATA_URL = 'http://www.mocky.io/v2/5b35cb7c2f0000692d3763c5'; 6 | const AwesomeImage = require('../assets/icon.png'); 7 | 8 | class FeedScreen extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | 12 | this.state = { 13 | isLoading: true, 14 | data: [], 15 | }; 16 | } 17 | 18 | componentDidMount() { 19 | this.callApi(); 20 | } 21 | 22 | callApi = async () => { 23 | try { 24 | const response = await fetch(GET_DATA_URL); 25 | const { data } = await response.json(); 26 | this.setState({ isLoading: false, data }); 27 | } catch (err) { 28 | console.warn(err); 29 | } 30 | } 31 | 32 | render() { 33 | const { navigation } = this.props; 34 | const { isLoading, data } = this.state; 35 | 36 | return ( 37 | 38 | 39 | 40 | 41 | Simple shopping app 42 | 43 | 44 | {isLoading 45 | ? 46 | : ( 47 | item.id} 50 | renderItem={({ item }) => ( 51 | 55 | )} 56 | /> 57 | )} 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default FeedScreen; 64 | 65 | const styles = StyleSheet.create({ 66 | container: { 67 | flex: 1, 68 | }, 69 | headerContainer: { 70 | backgroundColor: 'purple', 71 | padding: 8, 72 | flexDirection: 'row', 73 | alignItems: 'center', 74 | }, 75 | headerImage: { 76 | height: 32, 77 | width: 32, 78 | }, 79 | headerText: { 80 | color: 'white', 81 | marginLeft: 8, 82 | fontWeight: '400', 83 | fontSize: 16, 84 | }, 85 | activityIndicator: { 86 | paddingTop: 40, 87 | }, 88 | }); 89 | -------------------------------------------------------------------------------- /screens/LoginScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | KeyboardAvoidingView, 4 | Text, 5 | TouchableOpacity, 6 | TextInput, 7 | StyleSheet, 8 | Image, 9 | View, 10 | AsyncStorage, 11 | } from 'react-native'; 12 | 13 | const AwesomeImage = require('../assets/icon.png'); 14 | 15 | const USERNAME = 'admin'; 16 | const PASSWORD = 'password'; 17 | 18 | class LoginScreen extends React.Component { 19 | state = { 20 | isLoggedIn: false, 21 | currentUser: '', 22 | }; 23 | 24 | componentDidMount() { 25 | this.checkExistingSession(); 26 | } 27 | 28 | handleLogin = () => { 29 | const enteredUsername = this.usernameRef._lastNativeText; 30 | const enteredPassword = this.passwordRef._lastNativeText; 31 | 32 | if (enteredPassword === PASSWORD && enteredUsername === USERNAME) { 33 | this.setState({ isLoggedIn: true, currentUser: enteredUsername }); 34 | AsyncStorage.setItem('currentUser', enteredUsername); 35 | } 36 | } 37 | 38 | checkExistingSession = async () => { 39 | const currentUser = await AsyncStorage.getItem('currentUser'); 40 | if (currentUser) { 41 | this.setState({ isLoggedIn: true, currentUser }); 42 | } 43 | } 44 | 45 | handleLogout = () => { 46 | AsyncStorage.removeItem('currentUser'); 47 | alert('Logged out successfully!'); 48 | this.setState({ isLoggedIn: false, currentUser: '' }); 49 | } 50 | 51 | render() { 52 | const { isLoggedIn, currentUser } = this.state; 53 | 54 | return ( 55 | 56 | {!isLoggedIn 57 | ? ( 58 | 63 | 67 | { this.usernameRef = x; }} 69 | style={styles.textInput} 70 | placeholder="Username" 71 | maxLength={10} 72 | autoCapitalize="none" 73 | underlineColorAndroid="transparent" 74 | onSubmitEditing={() => this.passwordRef.focus()} 75 | returnKeyType="next" 76 | /> 77 | { this.passwordRef = x; }} 79 | style={styles.textInput} 80 | placeholder="Password" 81 | maxLength={32} 82 | secureTextEntry 83 | autoCapitalize="none" 84 | underlineColorAndroid="transparent" 85 | onSubmitEditing={this.handleLogin} 86 | returnKeyType="done" 87 | /> 88 | 89 | 93 | 94 | Login 95 | 96 | 97 | 98 | 99 | ) 100 | : ( 101 | 102 | 103 | {`Welcome ${currentUser}!`} 104 | 105 | 109 | 113 | 114 | Log out 115 | 116 | 117 | 118 | ) 119 | } 120 | 121 | ); 122 | } 123 | } 124 | 125 | export default LoginScreen; 126 | 127 | const styles = StyleSheet.create({ 128 | container: { 129 | flex: 1, 130 | }, 131 | form: { 132 | flex: 1, 133 | paddingTop: 40, 134 | paddingHorizontal: 16, 135 | alignItems: 'center', 136 | justifyContent: 'center', 137 | }, 138 | image: { 139 | alignSelf: 'center', 140 | height: 200, 141 | width: 200, 142 | }, 143 | textInput: { 144 | backgroundColor: 'white', 145 | marginTop: 24, 146 | padding: 8, 147 | height: 40, 148 | width: 240, 149 | borderRadius: 4, 150 | }, 151 | button: { 152 | backgroundColor: 'purple', 153 | padding: 8, 154 | borderRadius: 4, 155 | marginTop: 24, 156 | width: 240, 157 | alignItems: 'center', 158 | }, 159 | buttonText: { 160 | color: 'white', 161 | }, 162 | welcomeText: { 163 | fontSize: 16, 164 | }, 165 | logoutButton: { 166 | borderWidth: 1, 167 | borderColor: 'purple', 168 | borderRadius: 4, 169 | padding: 4, 170 | }, 171 | logoutText: { 172 | color: 'purple', 173 | }, 174 | }); 175 | -------------------------------------------------------------------------------- /screens/ProductScreen.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | ScrollView, 4 | View, 5 | Text, 6 | TouchableOpacity, 7 | StyleSheet, 8 | Image, 9 | Dimensions, 10 | } from 'react-native'; 11 | import Header from '../components/Header'; 12 | 13 | const WINDOW_WIDTH = Dimensions.get('window').width; 14 | 15 | class ProductScreen extends React.Component { 16 | handleBuyNow = () => { 17 | const { navigation: { state: { params } } } = this.props; 18 | alert(`Handle buy now for ${params.title}`); 19 | } 20 | 21 | render() { 22 | const { navigation: { state: { params } } } = this.props; 23 | const isSoldOut = params.availability === 'out of stock'; 24 | 25 | return ( 26 | 27 |
31 | 32 | 36 | 37 | {`₹ ${params.price} `} 38 | 39 | 40 | {params.description} 41 | 42 | 43 | 48 | 49 | {!isSoldOut ? 'Buy Now' : 'Sold Out'} 50 | 51 | 52 | 53 | ); 54 | } 55 | } 56 | 57 | export default ProductScreen; 58 | 59 | const styles = StyleSheet.create({ 60 | container: { 61 | flex: 1, 62 | backgroundColor: 'white', 63 | }, 64 | image: { 65 | width: WINDOW_WIDTH, 66 | height: WINDOW_WIDTH, 67 | }, 68 | price: { 69 | marginTop: 16, 70 | marginLeft: 16, 71 | fontWeight: '500', 72 | fontSize: 28, 73 | }, 74 | description: { 75 | padding: 16, 76 | fontSize: 16, 77 | }, 78 | buyButton: { 79 | backgroundColor: 'purple', 80 | justifyContent: 'center', 81 | alignItems: 'center', 82 | padding: 16, 83 | margin: 8, 84 | borderRadius: 4, 85 | }, 86 | buyButtonText: { 87 | color: 'white', 88 | fontSize: 16, 89 | }, 90 | }); 91 | --------------------------------------------------------------------------------