├── .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 |
--------------------------------------------------------------------------------