├── package.json ├── iOS ├── main.jsbundle ├── AppDelegate.h ├── main.m ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── AppDelegate.m └── Base.lproj │ └── LaunchScreen.xib ├── README.md ├── .gitignore ├── Featured.js ├── Search.js ├── .flowconfig ├── BookSearchTests ├── Info.plist └── BookSearchTests.m ├── BookDetail.js ├── index.ios.js ├── SearchResults.js ├── BookList.js ├── BookSearch.xcodeproj ├── xcshareddata │ └── xcschemes │ │ └── BookSearch.xcscheme └── project.pbxproj └── SearchBooks.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "BookSearch", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node_modules/react-native/packager/packager.sh" 7 | }, 8 | "dependencies": { 9 | "react-native": "^0.7.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /iOS/main.jsbundle: -------------------------------------------------------------------------------- 1 | // Offline JS 2 | // To re-generate the offline bundle, run this from the root of your project: 3 | // 4 | // $ react-native bundle --minify 5 | // 6 | // See http://facebook.github.io/react-native/docs/runningondevice.html for more details. 7 | 8 | throw new Error('Offline JS file is empty. See iOS/main.jsbundle for instructions'); 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Demo App for iOS 2 | 3 | Facebook open sourced React Native which is a framework that lets you 4 | build native iOS and Android (at the moment Android support is still under 5 | development) applications with JavaScript. 6 | 7 | This is a simple demo app built with React Native. For details, please refer to this tutorial: 8 | 9 | http://www.appcoda.com/react-native-introduction 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # node.js 26 | # 27 | node_modules/ 28 | npm-debug.log 29 | -------------------------------------------------------------------------------- /iOS/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /iOS/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Featured.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by echessa on 4/24/15. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var React = require('react-native'); 8 | var BookList = require('./BookList'); 9 | 10 | var { 11 | StyleSheet, 12 | NavigatorIOS, 13 | Component 14 | } = React; 15 | 16 | var styles = StyleSheet.create({ 17 | container: { 18 | flex: 1 19 | } 20 | }); 21 | 22 | class Featured extends Component { 23 | render() { 24 | return ( 25 | 31 | ); 32 | } 33 | } 34 | 35 | module.exports = Featured; -------------------------------------------------------------------------------- /Search.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by echessa on 4/24/15. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var React = require('react-native'); 8 | var SearchBooks = require('./SearchBooks'); 9 | 10 | var { 11 | StyleSheet, 12 | NavigatorIOS, 13 | Component 14 | } = React; 15 | 16 | var styles = StyleSheet.create({ 17 | container: { 18 | flex: 1 19 | } 20 | }); 21 | 22 | class Search extends Component { 23 | render() { 24 | return ( 25 | 31 | ); 32 | } 33 | } 34 | 35 | module.exports = Search; -------------------------------------------------------------------------------- /iOS/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*.web.js 5 | .*/*.android.js 6 | 7 | # Some modules have their own node_modules with overlap 8 | .*/node_modules/node-haste/.* 9 | 10 | # Ignore react-tools where there are overlaps, but don't ignore anything that 11 | # react-native relies on 12 | .*/node_modules/react-tools/src/vendor/core/ExecutionEnvironment.js 13 | .*/node_modules/react-tools/src/browser/eventPlugins/ResponderEventPlugin.js 14 | .*/node_modules/react-tools/src/browser/ui/React.js 15 | .*/node_modules/react-tools/src/core/ReactInstanceHandles.js 16 | .*/node_modules/react-tools/src/event/EventPropagators.js 17 | 18 | # Ignore jest 19 | .*/react-native/node_modules/jest-cli/.* 20 | 21 | [include] 22 | 23 | [libs] 24 | node_modules/react-native/Libraries/react-native/react-native-interface.js 25 | 26 | [options] 27 | module.system=haste 28 | -------------------------------------------------------------------------------- /BookSearchTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /BookDetail.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by echessa on 4/24/15. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var React = require('react-native'); 8 | 9 | var { 10 | StyleSheet, 11 | Text, 12 | View, 13 | Component, 14 | Image 15 | } = React; 16 | 17 | var styles = StyleSheet.create({ 18 | container: { 19 | marginTop: 75, 20 | alignItems: 'center' 21 | }, 22 | image: { 23 | width: 107, 24 | height: 165, 25 | padding: 10 26 | }, 27 | description: { 28 | padding: 10, 29 | fontSize: 15, 30 | color: '#656565' 31 | } 32 | }); 33 | 34 | class BookDetail extends Component { 35 | render() { 36 | var book = this.props.book; 37 | var imageURI = (typeof book.volumeInfo.imageLinks !== 'undefined') ? book.volumeInfo.imageLinks.thumbnail : ''; 38 | var description = (typeof book.volumeInfo.description !== 'undefined') ? book.volumeInfo.description : ''; 39 | return ( 40 | 41 | 42 | {description} 43 | 44 | ); 45 | } 46 | } 47 | 48 | module.exports = BookDetail; -------------------------------------------------------------------------------- /index.ios.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react-native'); 4 | var Featured = require('./Featured'); 5 | var Search = require('./Search'); 6 | 7 | var { 8 | AppRegistry, 9 | TabBarIOS, 10 | Component 11 | } = React; 12 | 13 | class BookSearch extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | selectedTab: 'featured' 19 | }; 20 | } 21 | 22 | render() { 23 | return ( 24 | 25 | { 29 | this.setState({ 30 | selectedTab: 'featured' 31 | }); 32 | }}> 33 | 34 | 35 | { 39 | this.setState({ 40 | selectedTab: 'search' 41 | }); 42 | }}> 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | 51 | AppRegistry.registerComponent('BookSearch', () => BookSearch); -------------------------------------------------------------------------------- /iOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSAllowsArbitraryLoads 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /BookSearchTests/BookSearchTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import "RCTAssert.h" 14 | #import "RCTRedBox.h" 15 | #import "RCTRootView.h" 16 | 17 | #define TIMEOUT_SECONDS 240 18 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 19 | 20 | @interface BookSearchTests : XCTestCase 21 | 22 | @end 23 | 24 | @implementation BookSearchTests 25 | 26 | 27 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 28 | { 29 | if (test(view)) { 30 | return YES; 31 | } 32 | for (UIView *subview in [view subviews]) { 33 | if ([self findSubviewInView:subview matching:test]) { 34 | return YES; 35 | } 36 | } 37 | return NO; 38 | } 39 | 40 | - (void)testRendersWelcomeScreen { 41 | UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 42 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 43 | BOOL foundElement = NO; 44 | NSString *redboxError = nil; 45 | 46 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 47 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 48 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 49 | 50 | redboxError = [[RCTRedBox sharedInstance] currentErrorMessage]; 51 | 52 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 53 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 54 | return YES; 55 | } 56 | return NO; 57 | }]; 58 | } 59 | 60 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 61 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 62 | } 63 | 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /iOS/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import "RCTRootView.h" 13 | 14 | @implementation AppDelegate 15 | 16 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 17 | { 18 | NSURL *jsCodeLocation; 19 | 20 | /** 21 | * Loading JavaScript code - uncomment the one you want. 22 | * 23 | * OPTION 1 24 | * Load from development server. Start the server from the repository root: 25 | * 26 | * $ npm start 27 | * 28 | * To run on device, change `localhost` to the IP address of your computer 29 | * (you can get this by typing `ifconfig` into the terminal and selecting the 30 | * `inet` value under `en0:`) and make sure your computer and iOS device are 31 | * on the same Wi-Fi network. 32 | */ 33 | 34 | jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle"]; 35 | 36 | /** 37 | * OPTION 2 38 | * Load from pre-bundled file on disk. To re-generate the static bundle 39 | * from the root of your project directory, run 40 | * 41 | * $ react-native bundle --minify 42 | * 43 | * see http://facebook.github.io/react-native/docs/runningondevice.html 44 | */ 45 | 46 | // jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; 47 | 48 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 49 | moduleName:@"BookSearch" 50 | launchOptions:launchOptions]; 51 | 52 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 53 | UIViewController *rootViewController = [[UIViewController alloc] init]; 54 | rootViewController.view = rootView; 55 | self.window.rootViewController = rootViewController; 56 | [self.window makeKeyAndVisible]; 57 | return YES; 58 | } 59 | 60 | @end 61 | -------------------------------------------------------------------------------- /SearchResults.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by echessa on 4/24/15. 3 | */ 4 | 5 | 6 | 'use strict'; 7 | 8 | var React = require('react-native'); 9 | var BookDetail = require('./BookDetail'); 10 | var { 11 | StyleSheet, 12 | View, 13 | Text, 14 | Component, 15 | TouchableHighlight, 16 | Image, 17 | ListView 18 | } = React; 19 | 20 | var styles = StyleSheet.create({ 21 | container: { 22 | flex: 1, 23 | justifyContent: 'center', 24 | alignItems: 'center' 25 | }, 26 | title: { 27 | fontSize: 20, 28 | marginBottom: 8 29 | }, 30 | author: { 31 | color: '#656565' 32 | }, 33 | separator: { 34 | height: 1, 35 | backgroundColor: '#dddddd' 36 | }, 37 | listView: { 38 | backgroundColor: '#F5FCFF' 39 | }, 40 | cellContainer: { 41 | flex: 1, 42 | flexDirection: 'row', 43 | justifyContent: 'center', 44 | alignItems: 'center', 45 | backgroundColor: '#F5FCFF', 46 | padding: 10 47 | }, 48 | thumbnail: { 49 | width: 53, 50 | height: 81, 51 | marginRight: 10 52 | }, 53 | rightContainer: { 54 | flex: 1 55 | } 56 | }); 57 | 58 | class SearchResults extends Component { 59 | 60 | constructor(props) { 61 | super(props); 62 | 63 | var dataSource = new ListView.DataSource( 64 | {rowHasChanged: (row1, row2) => row1 !== row2}); 65 | this.state = { 66 | dataSource: dataSource.cloneWithRows(this.props.books) 67 | }; 68 | } 69 | 70 | render() { 71 | 72 | return ( 73 | 78 | ); 79 | } 80 | 81 | renderBook(book) { 82 | var imageURI = (typeof book.volumeInfo.imageLinks !== 'undefined') ? book.volumeInfo.imageLinks.thumbnail : ''; 83 | 84 | return ( 85 | this.showBookDetail(book)} 86 | underlayColor='#dddddd'> 87 | 88 | 89 | 92 | 93 | {book.volumeInfo.title} 94 | {book.volumeInfo.authors} 95 | 96 | 97 | 98 | 99 | 100 | ); 101 | } 102 | 103 | showBookDetail(book) { 104 | 105 | this.props.navigator.push({ 106 | title: book.volumeInfo.title, 107 | component: BookDetail, 108 | passProps: {book} 109 | }); 110 | } 111 | 112 | } 113 | 114 | module.exports = SearchResults; -------------------------------------------------------------------------------- /iOS/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /BookList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by echessa on 4/24/15. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var React = require('react-native'); 8 | var REQUEST_URL = 'https://www.googleapis.com/books/v1/volumes?q=subject:fiction'; 9 | var BookDetail = require('./BookDetail'); 10 | 11 | var { 12 | Image, 13 | StyleSheet, 14 | Text, 15 | View, 16 | Component, 17 | ListView, 18 | TouchableHighlight, 19 | ActivityIndicatorIOS 20 | } = React; 21 | 22 | var styles = StyleSheet.create({ 23 | container: { 24 | flex: 1, 25 | flexDirection: 'row', 26 | justifyContent: 'center', 27 | alignItems: 'center', 28 | backgroundColor: '#F5FCFF', 29 | padding: 10 30 | }, 31 | thumbnail: { 32 | width: 53, 33 | height: 81, 34 | marginRight: 10 35 | }, 36 | rightContainer: { 37 | flex: 1 38 | }, 39 | title: { 40 | fontSize: 20, 41 | marginBottom: 8 42 | }, 43 | author: { 44 | color: '#656565' 45 | }, 46 | separator: { 47 | height: 1, 48 | backgroundColor: '#dddddd' 49 | }, 50 | listView: { 51 | backgroundColor: '#F5FCFF' 52 | }, 53 | loading: { 54 | flex: 1, 55 | alignItems: 'center', 56 | justifyContent: 'center' 57 | } 58 | }); 59 | 60 | class BookList extends Component { 61 | 62 | constructor(props) { 63 | super(props); 64 | this.state = { 65 | isLoading: true, 66 | dataSource: new ListView.DataSource({ 67 | rowHasChanged: (row1, row2) => row1 !== row2 68 | }) 69 | }; 70 | } 71 | 72 | componentDidMount() { 73 | this.fetchData(); 74 | } 75 | 76 | fetchData() { 77 | 78 | fetch(REQUEST_URL) 79 | .then((response) => response.json()) 80 | .then((responseData) => { 81 | this.setState({ 82 | dataSource: this.state.dataSource.cloneWithRows(responseData.items), 83 | isLoading: false 84 | }); 85 | }) 86 | .done(); 87 | } 88 | 89 | render() { 90 | if (this.state.isLoading) { 91 | return this.renderLoadingView(); 92 | } 93 | 94 | return ( 95 | 100 | ); 101 | } 102 | 103 | renderBook(book) { 104 | return ( 105 | this.showBookDetail(book)} underlayColor='#dddddd'> 106 | 107 | 108 | 111 | 112 | {book.volumeInfo.title} 113 | {book.volumeInfo.authors} 114 | 115 | 116 | 117 | 118 | 119 | ); 120 | } 121 | 122 | renderLoadingView() { 123 | return ( 124 | 125 | 127 | 128 | Loading books... 129 | 130 | 131 | ); 132 | } 133 | 134 | showBookDetail(book) { 135 | 136 | this.props.navigator.push({ 137 | title: book.volumeInfo.title, 138 | component: BookDetail, 139 | passProps: {book} 140 | }); 141 | } 142 | 143 | 144 | } 145 | 146 | module.exports = BookList; -------------------------------------------------------------------------------- /BookSearch.xcodeproj/xcshareddata/xcschemes/BookSearch.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /SearchBooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by echessa on 4/24/15. 3 | */ 4 | 5 | 'use strict'; 6 | 7 | var React = require('react-native'); 8 | var SearchResults = require('./SearchResults'); 9 | var { 10 | StyleSheet, 11 | View, 12 | Text, 13 | Component, 14 | TextInput, 15 | TouchableHighlight, 16 | ActivityIndicatorIOS 17 | } = React; 18 | 19 | var styles = StyleSheet.create({ 20 | container: { 21 | marginTop: 65, 22 | padding: 10 23 | }, 24 | searchInput: { 25 | height: 36, 26 | marginTop: 10, 27 | marginBottom: 10, 28 | fontSize: 18, 29 | borderWidth: 1, 30 | flex: 1, 31 | borderRadius: 4, 32 | padding: 5 33 | }, 34 | button: { 35 | height: 36, 36 | backgroundColor: '#f39c12', 37 | borderRadius: 8, 38 | justifyContent: 'center', 39 | marginTop: 15 40 | }, 41 | buttonText: { 42 | fontSize: 18, 43 | color: 'white', 44 | alignSelf: 'center' 45 | }, 46 | instructions: { 47 | fontSize: 18, 48 | alignSelf: 'center', 49 | marginBottom: 15 50 | }, 51 | fieldLabel: { 52 | fontSize: 15, 53 | marginTop: 15 54 | }, 55 | errorMessage: { 56 | fontSize: 15, 57 | alignSelf: 'center', 58 | marginTop: 15, 59 | color: 'red' 60 | } 61 | }); 62 | 63 | class SearchBooks extends Component { 64 | 65 | constructor(props) { 66 | super(props); 67 | this.state = { 68 | bookAuthor: '', 69 | bookTitle: '', 70 | isLoading: false, 71 | errorMessage: '' 72 | }; 73 | } 74 | 75 | 76 | render() { 77 | var spinner = this.state.isLoading ? 78 | (