├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── StorekeeperApp ├── .flowconfig ├── .gitignore ├── .npmignore ├── StorekeeperApp.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── StorekeeperApp.xcscheme ├── StorekeeperAppTests │ ├── Info.plist │ └── StorekeeperAppTests.m ├── iOS │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── main.jsbundle │ └── main.m ├── index.ios.js ├── package.json └── src │ ├── actions │ └── orders_actions.js │ ├── alt.js │ ├── components │ └── latest_orders.js │ ├── lib │ └── formatters.js │ ├── stores │ └── orders_store.js │ └── utils │ ├── orders_fetcher.js │ └── parse_orders.js ├── data ├── orders.json └── sales_stats.json ├── digest_email ├── digest_email.js ├── render.js ├── sales_sparkline.js ├── send.js └── styles.css ├── package.json ├── server.js ├── src ├── actions │ ├── dashboard_actions.js │ └── orders_actions.js ├── alt.js ├── client.js ├── components │ ├── app.js │ ├── dashboard.js │ ├── main_header.js │ ├── main_nav.js │ ├── not_found.js │ ├── orders.js │ ├── orders │ │ ├── order_row.js │ │ └── orders_table.js │ ├── page.js │ ├── page_header.js │ ├── sales_chart.js │ ├── sales_chart_x_axis.js │ └── sales_chart_y_axis.js ├── data │ └── orders.js ├── lib │ └── formatters.js ├── routes.js ├── stores │ ├── dashboard_store.js │ └── orders_store.js └── utils │ ├── orders_fetcher.js │ ├── parse_orders.js │ └── sales_stats_fetcher.js ├── styles ├── _base.scss ├── _layout.scss ├── main.scss ├── modules │ ├── _dashboard.scss │ ├── _main_header.scss │ ├── _orders.scss │ └── _page.scss └── theme.scss ├── test ├── components │ ├── orders │ │ └── order_row_test.js │ └── orders_test.js └── test_helper.js └── views └── index.ejs /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/node-sass/.* 3 | .*/src/.* 4 | 5 | [include] 6 | 7 | [libs] 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | .DS_Store 29 | 30 | public/bundle.js 31 | public/main.css 32 | build_flow 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Simon Højberg 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mastering-react 2 | Mastering React sample application 3 | 4 | # Installation on Mac for sending emails 5 | 1. Install librsvg: `brew install librsvg` 6 | 2. Install [XQuartz](http://xquartz.macosforge.org/landing/) 7 | 3. `export PKG_CONFIG_PATH=/opt/X11/lib/pkgconfig` 8 | -------------------------------------------------------------------------------- /StorekeeperApp/.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/React.js 13 | .*/node_modules/react-tools/src/renderers/shared/reconciler/ReactInstanceHandles.js 14 | .*/node_modules/react-tools/src/renderers/shared/event/EventPropagators.js 15 | .*/node_modules/react-tools/src/renderers/shared/event/eventPlugins/ResponderEventPlugin.js 16 | .*/node_modules/react-tools/src/renderers/shared/event/eventPlugins/ResponderSyntheticEvent.js 17 | .*/node_modules/react-tools/src/renderers/shared/event/eventPlugins/ResponderTouchHistoryStore.js 18 | .*/node_modules/react-tools/src/shared/vendor/core/ExecutionEnvironment.js 19 | 20 | 21 | # Ignore commoner tests 22 | .*/node_modules/commoner/test/.* 23 | 24 | # See https://github.com/facebook/flow/issues/442 25 | .*/react-tools/node_modules/commoner/lib/reader.js 26 | 27 | # Ignore jest 28 | .*/react-native/node_modules/jest-cli/.* 29 | 30 | [include] 31 | 32 | [libs] 33 | node_modules/react-native/Libraries/react-native/react-native-interface.js 34 | 35 | [options] 36 | module.system=haste 37 | 38 | suppress_type=$FlowIssue 39 | suppress_type=$FlowFixMe 40 | suppress_type=$FixMe 41 | 42 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 43 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-3]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ 44 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 45 | 46 | [version] 47 | 0.13.1 48 | -------------------------------------------------------------------------------- /StorekeeperApp/.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 | -------------------------------------------------------------------------------- /StorekeeperApp/.npmignore: -------------------------------------------------------------------------------- 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 | 24 | # node.js 25 | # 26 | node_modules/ 27 | npm-debug.log 28 | -------------------------------------------------------------------------------- /StorekeeperApp/StorekeeperApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */ = {isa = PBXBuildFile; fileRef = 008F07F21AC5B25A0029DE68 /* main.jsbundle */; }; 11 | 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 12 | 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; 13 | 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 14 | 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 15 | 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; 16 | 00E356F31AD99517003FC87E /* StorekeeperAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* StorekeeperAppTests.m */; }; 17 | 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 18 | 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 19 | 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; 20 | 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 21 | 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 22 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 23 | 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 24 | 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 25 | 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; 32 | proxyType = 2; 33 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 34 | remoteInfo = RCTActionSheet; 35 | }; 36 | 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; 39 | proxyType = 2; 40 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 41 | remoteInfo = RCTGeolocation; 42 | }; 43 | 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; 46 | proxyType = 2; 47 | remoteGlobalIDString = 58B5115D1A9E6B3D00147676; 48 | remoteInfo = RCTImage; 49 | }; 50 | 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { 51 | isa = PBXContainerItemProxy; 52 | containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; 53 | proxyType = 2; 54 | remoteGlobalIDString = 58B511DB1A9E6C8500147676; 55 | remoteInfo = RCTNetwork; 56 | }; 57 | 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { 58 | isa = PBXContainerItemProxy; 59 | containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; 60 | proxyType = 2; 61 | remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; 62 | remoteInfo = RCTVibration; 63 | }; 64 | 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { 65 | isa = PBXContainerItemProxy; 66 | containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; 67 | proxyType = 1; 68 | remoteGlobalIDString = 13B07F861A680F5B00A75B9A; 69 | remoteInfo = StorekeeperApp; 70 | }; 71 | 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { 72 | isa = PBXContainerItemProxy; 73 | containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; 74 | proxyType = 2; 75 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 76 | remoteInfo = RCTSettings; 77 | }; 78 | 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { 79 | isa = PBXContainerItemProxy; 80 | containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; 81 | proxyType = 2; 82 | remoteGlobalIDString = 3C86DF461ADF2C930047B81A; 83 | remoteInfo = RCTWebSocket; 84 | }; 85 | 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { 86 | isa = PBXContainerItemProxy; 87 | containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; 88 | proxyType = 2; 89 | remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; 90 | remoteInfo = React; 91 | }; 92 | 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { 93 | isa = PBXContainerItemProxy; 94 | containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; 95 | proxyType = 2; 96 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 97 | remoteInfo = RCTLinking; 98 | }; 99 | 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { 100 | isa = PBXContainerItemProxy; 101 | containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; 102 | proxyType = 2; 103 | remoteGlobalIDString = 58B5119B1A9E6C1200147676; 104 | remoteInfo = RCTText; 105 | }; 106 | /* End PBXContainerItemProxy section */ 107 | 108 | /* Begin PBXFileReference section */ 109 | 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = iOS/main.jsbundle; sourceTree = ""; }; 110 | 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 111 | 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; 112 | 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = node_modules/react-native/Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 113 | 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; 114 | 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; 115 | 00E356EE1AD99517003FC87E /* StorekeeperAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StorekeeperAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 116 | 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 117 | 00E356F21AD99517003FC87E /* StorekeeperAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StorekeeperAppTests.m; sourceTree = ""; }; 118 | 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 119 | 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 120 | 13B07F961A680F5B00A75B9A /* StorekeeperApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StorekeeperApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 121 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iOS/AppDelegate.h; sourceTree = ""; }; 122 | 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = iOS/AppDelegate.m; sourceTree = ""; }; 123 | 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 124 | 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = iOS/Images.xcassets; sourceTree = ""; }; 125 | 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = iOS/Info.plist; sourceTree = ""; }; 126 | 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = iOS/main.m; sourceTree = ""; }; 127 | 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = node_modules/react-native/React/React.xcodeproj; sourceTree = ""; }; 128 | 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 129 | 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = node_modules/react-native/Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; 130 | /* End PBXFileReference section */ 131 | 132 | /* Begin PBXFrameworksBuildPhase section */ 133 | 00E356EB1AD99517003FC87E /* Frameworks */ = { 134 | isa = PBXFrameworksBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | ); 138 | runOnlyForDeploymentPostprocessing = 0; 139 | }; 140 | 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { 141 | isa = PBXFrameworksBuildPhase; 142 | buildActionMask = 2147483647; 143 | files = ( 144 | 146834051AC3E58100842450 /* libReact.a in Frameworks */, 145 | 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 146 | 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, 147 | 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, 148 | 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, 149 | 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, 150 | 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, 151 | 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, 152 | 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 153 | 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, 154 | ); 155 | runOnlyForDeploymentPostprocessing = 0; 156 | }; 157 | /* End PBXFrameworksBuildPhase section */ 158 | 159 | /* Begin PBXGroup section */ 160 | 00C302A81ABCB8CE00DB3ED1 /* Products */ = { 161 | isa = PBXGroup; 162 | children = ( 163 | 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, 164 | ); 165 | name = Products; 166 | sourceTree = ""; 167 | }; 168 | 00C302B61ABCB90400DB3ED1 /* Products */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, 172 | ); 173 | name = Products; 174 | sourceTree = ""; 175 | }; 176 | 00C302BC1ABCB91800DB3ED1 /* Products */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, 180 | ); 181 | name = Products; 182 | sourceTree = ""; 183 | }; 184 | 00C302D41ABCB9D200DB3ED1 /* Products */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, 188 | ); 189 | name = Products; 190 | sourceTree = ""; 191 | }; 192 | 00C302E01ABCB9EE00DB3ED1 /* Products */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, 196 | ); 197 | name = Products; 198 | sourceTree = ""; 199 | }; 200 | 00E356EF1AD99517003FC87E /* StorekeeperAppTests */ = { 201 | isa = PBXGroup; 202 | children = ( 203 | 00E356F21AD99517003FC87E /* StorekeeperAppTests.m */, 204 | 00E356F01AD99517003FC87E /* Supporting Files */, 205 | ); 206 | path = StorekeeperAppTests; 207 | sourceTree = ""; 208 | }; 209 | 00E356F01AD99517003FC87E /* Supporting Files */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 00E356F11AD99517003FC87E /* Info.plist */, 213 | ); 214 | name = "Supporting Files"; 215 | sourceTree = ""; 216 | }; 217 | 139105B71AF99BAD00B5F7CC /* Products */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, 221 | ); 222 | name = Products; 223 | sourceTree = ""; 224 | }; 225 | 139FDEE71B06529A00C62182 /* Products */ = { 226 | isa = PBXGroup; 227 | children = ( 228 | 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, 229 | ); 230 | name = Products; 231 | sourceTree = ""; 232 | }; 233 | 13B07FAE1A68108700A75B9A /* StorekeeperApp */ = { 234 | isa = PBXGroup; 235 | children = ( 236 | 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 237 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 238 | 13B07FB01A68108700A75B9A /* AppDelegate.m */, 239 | 13B07FB51A68108700A75B9A /* Images.xcassets */, 240 | 13B07FB61A68108700A75B9A /* Info.plist */, 241 | 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 242 | 13B07FB71A68108700A75B9A /* main.m */, 243 | ); 244 | name = StorekeeperApp; 245 | sourceTree = ""; 246 | }; 247 | 146834001AC3E56700842450 /* Products */ = { 248 | isa = PBXGroup; 249 | children = ( 250 | 146834041AC3E56700842450 /* libReact.a */, 251 | ); 252 | name = Products; 253 | sourceTree = ""; 254 | }; 255 | 78C398B11ACF4ADC00677621 /* Products */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, 259 | ); 260 | name = Products; 261 | sourceTree = ""; 262 | }; 263 | 832341AE1AAA6A7D00B99B32 /* Libraries */ = { 264 | isa = PBXGroup; 265 | children = ( 266 | 146833FF1AC3E56700842450 /* React.xcodeproj */, 267 | 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, 268 | 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 269 | 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, 270 | 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 271 | 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 272 | 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, 273 | 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 274 | 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 275 | 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, 276 | ); 277 | name = Libraries; 278 | sourceTree = ""; 279 | }; 280 | 832341B11AAA6A8300B99B32 /* Products */ = { 281 | isa = PBXGroup; 282 | children = ( 283 | 832341B51AAA6A8300B99B32 /* libRCTText.a */, 284 | ); 285 | name = Products; 286 | sourceTree = ""; 287 | }; 288 | 83CBB9F61A601CBA00E9B192 = { 289 | isa = PBXGroup; 290 | children = ( 291 | 13B07FAE1A68108700A75B9A /* StorekeeperApp */, 292 | 832341AE1AAA6A7D00B99B32 /* Libraries */, 293 | 00E356EF1AD99517003FC87E /* StorekeeperAppTests */, 294 | 83CBBA001A601CBA00E9B192 /* Products */, 295 | ); 296 | indentWidth = 2; 297 | sourceTree = ""; 298 | tabWidth = 2; 299 | }; 300 | 83CBBA001A601CBA00E9B192 /* Products */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | 13B07F961A680F5B00A75B9A /* StorekeeperApp.app */, 304 | 00E356EE1AD99517003FC87E /* StorekeeperAppTests.xctest */, 305 | ); 306 | name = Products; 307 | sourceTree = ""; 308 | }; 309 | /* End PBXGroup section */ 310 | 311 | /* Begin PBXNativeTarget section */ 312 | 00E356ED1AD99517003FC87E /* StorekeeperAppTests */ = { 313 | isa = PBXNativeTarget; 314 | buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "StorekeeperAppTests" */; 315 | buildPhases = ( 316 | 00E356EA1AD99517003FC87E /* Sources */, 317 | 00E356EB1AD99517003FC87E /* Frameworks */, 318 | 00E356EC1AD99517003FC87E /* Resources */, 319 | ); 320 | buildRules = ( 321 | ); 322 | dependencies = ( 323 | 00E356F51AD99517003FC87E /* PBXTargetDependency */, 324 | ); 325 | name = StorekeeperAppTests; 326 | productName = StorekeeperAppTests; 327 | productReference = 00E356EE1AD99517003FC87E /* StorekeeperAppTests.xctest */; 328 | productType = "com.apple.product-type.bundle.unit-test"; 329 | }; 330 | 13B07F861A680F5B00A75B9A /* StorekeeperApp */ = { 331 | isa = PBXNativeTarget; 332 | buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "StorekeeperApp" */; 333 | buildPhases = ( 334 | 13B07F871A680F5B00A75B9A /* Sources */, 335 | 13B07F8C1A680F5B00A75B9A /* Frameworks */, 336 | 13B07F8E1A680F5B00A75B9A /* Resources */, 337 | ); 338 | buildRules = ( 339 | ); 340 | dependencies = ( 341 | ); 342 | name = StorekeeperApp; 343 | productName = "Hello World"; 344 | productReference = 13B07F961A680F5B00A75B9A /* StorekeeperApp.app */; 345 | productType = "com.apple.product-type.application"; 346 | }; 347 | /* End PBXNativeTarget section */ 348 | 349 | /* Begin PBXProject section */ 350 | 83CBB9F71A601CBA00E9B192 /* Project object */ = { 351 | isa = PBXProject; 352 | attributes = { 353 | LastUpgradeCheck = 0610; 354 | ORGANIZATIONNAME = Facebook; 355 | TargetAttributes = { 356 | 00E356ED1AD99517003FC87E = { 357 | CreatedOnToolsVersion = 6.2; 358 | TestTargetID = 13B07F861A680F5B00A75B9A; 359 | }; 360 | }; 361 | }; 362 | buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "StorekeeperApp" */; 363 | compatibilityVersion = "Xcode 3.2"; 364 | developmentRegion = English; 365 | hasScannedForEncodings = 0; 366 | knownRegions = ( 367 | en, 368 | Base, 369 | ); 370 | mainGroup = 83CBB9F61A601CBA00E9B192; 371 | productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; 372 | projectDirPath = ""; 373 | projectReferences = ( 374 | { 375 | ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; 376 | ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; 377 | }, 378 | { 379 | ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; 380 | ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; 381 | }, 382 | { 383 | ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; 384 | ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; 385 | }, 386 | { 387 | ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; 388 | ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; 389 | }, 390 | { 391 | ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; 392 | ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; 393 | }, 394 | { 395 | ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; 396 | ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; 397 | }, 398 | { 399 | ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; 400 | ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; 401 | }, 402 | { 403 | ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; 404 | ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; 405 | }, 406 | { 407 | ProductGroup = 139FDEE71B06529A00C62182 /* Products */; 408 | ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; 409 | }, 410 | { 411 | ProductGroup = 146834001AC3E56700842450 /* Products */; 412 | ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; 413 | }, 414 | ); 415 | projectRoot = ""; 416 | targets = ( 417 | 13B07F861A680F5B00A75B9A /* StorekeeperApp */, 418 | 00E356ED1AD99517003FC87E /* StorekeeperAppTests */, 419 | ); 420 | }; 421 | /* End PBXProject section */ 422 | 423 | /* Begin PBXReferenceProxy section */ 424 | 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { 425 | isa = PBXReferenceProxy; 426 | fileType = archive.ar; 427 | path = libRCTActionSheet.a; 428 | remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; 429 | sourceTree = BUILT_PRODUCTS_DIR; 430 | }; 431 | 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { 432 | isa = PBXReferenceProxy; 433 | fileType = archive.ar; 434 | path = libRCTGeolocation.a; 435 | remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; 436 | sourceTree = BUILT_PRODUCTS_DIR; 437 | }; 438 | 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { 439 | isa = PBXReferenceProxy; 440 | fileType = archive.ar; 441 | path = libRCTImage.a; 442 | remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; 443 | sourceTree = BUILT_PRODUCTS_DIR; 444 | }; 445 | 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { 446 | isa = PBXReferenceProxy; 447 | fileType = archive.ar; 448 | path = libRCTNetwork.a; 449 | remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; 450 | sourceTree = BUILT_PRODUCTS_DIR; 451 | }; 452 | 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { 453 | isa = PBXReferenceProxy; 454 | fileType = archive.ar; 455 | path = libRCTVibration.a; 456 | remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; 457 | sourceTree = BUILT_PRODUCTS_DIR; 458 | }; 459 | 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { 460 | isa = PBXReferenceProxy; 461 | fileType = archive.ar; 462 | path = libRCTSettings.a; 463 | remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; 464 | sourceTree = BUILT_PRODUCTS_DIR; 465 | }; 466 | 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { 467 | isa = PBXReferenceProxy; 468 | fileType = archive.ar; 469 | path = libRCTWebSocket.a; 470 | remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; 471 | sourceTree = BUILT_PRODUCTS_DIR; 472 | }; 473 | 146834041AC3E56700842450 /* libReact.a */ = { 474 | isa = PBXReferenceProxy; 475 | fileType = archive.ar; 476 | path = libReact.a; 477 | remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; 478 | sourceTree = BUILT_PRODUCTS_DIR; 479 | }; 480 | 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { 481 | isa = PBXReferenceProxy; 482 | fileType = archive.ar; 483 | path = libRCTLinking.a; 484 | remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; 485 | sourceTree = BUILT_PRODUCTS_DIR; 486 | }; 487 | 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { 488 | isa = PBXReferenceProxy; 489 | fileType = archive.ar; 490 | path = libRCTText.a; 491 | remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; 492 | sourceTree = BUILT_PRODUCTS_DIR; 493 | }; 494 | /* End PBXReferenceProxy section */ 495 | 496 | /* Begin PBXResourcesBuildPhase section */ 497 | 00E356EC1AD99517003FC87E /* Resources */ = { 498 | isa = PBXResourcesBuildPhase; 499 | buildActionMask = 2147483647; 500 | files = ( 501 | ); 502 | runOnlyForDeploymentPostprocessing = 0; 503 | }; 504 | 13B07F8E1A680F5B00A75B9A /* Resources */ = { 505 | isa = PBXResourcesBuildPhase; 506 | buildActionMask = 2147483647; 507 | files = ( 508 | 008F07F31AC5B25A0029DE68 /* main.jsbundle in Resources */, 509 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 510 | 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 511 | ); 512 | runOnlyForDeploymentPostprocessing = 0; 513 | }; 514 | /* End PBXResourcesBuildPhase section */ 515 | 516 | /* Begin PBXSourcesBuildPhase section */ 517 | 00E356EA1AD99517003FC87E /* Sources */ = { 518 | isa = PBXSourcesBuildPhase; 519 | buildActionMask = 2147483647; 520 | files = ( 521 | 00E356F31AD99517003FC87E /* StorekeeperAppTests.m in Sources */, 522 | ); 523 | runOnlyForDeploymentPostprocessing = 0; 524 | }; 525 | 13B07F871A680F5B00A75B9A /* Sources */ = { 526 | isa = PBXSourcesBuildPhase; 527 | buildActionMask = 2147483647; 528 | files = ( 529 | 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 530 | 13B07FC11A68108700A75B9A /* main.m in Sources */, 531 | ); 532 | runOnlyForDeploymentPostprocessing = 0; 533 | }; 534 | /* End PBXSourcesBuildPhase section */ 535 | 536 | /* Begin PBXTargetDependency section */ 537 | 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { 538 | isa = PBXTargetDependency; 539 | target = 13B07F861A680F5B00A75B9A /* StorekeeperApp */; 540 | targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; 541 | }; 542 | /* End PBXTargetDependency section */ 543 | 544 | /* Begin PBXVariantGroup section */ 545 | 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { 546 | isa = PBXVariantGroup; 547 | children = ( 548 | 13B07FB21A68108700A75B9A /* Base */, 549 | ); 550 | name = LaunchScreen.xib; 551 | path = iOS; 552 | sourceTree = ""; 553 | }; 554 | /* End PBXVariantGroup section */ 555 | 556 | /* Begin XCBuildConfiguration section */ 557 | 00E356F61AD99517003FC87E /* Debug */ = { 558 | isa = XCBuildConfiguration; 559 | buildSettings = { 560 | BUNDLE_LOADER = "$(TEST_HOST)"; 561 | FRAMEWORK_SEARCH_PATHS = ( 562 | "$(SDKROOT)/Developer/Library/Frameworks", 563 | "$(inherited)", 564 | ); 565 | GCC_PREPROCESSOR_DEFINITIONS = ( 566 | "DEBUG=1", 567 | "$(inherited)", 568 | ); 569 | INFOPLIST_FILE = StorekeeperAppTests/Info.plist; 570 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 571 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 572 | PRODUCT_NAME = "$(TARGET_NAME)"; 573 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StorekeeperApp.app/StorekeeperApp"; 574 | }; 575 | name = Debug; 576 | }; 577 | 00E356F71AD99517003FC87E /* Release */ = { 578 | isa = XCBuildConfiguration; 579 | buildSettings = { 580 | BUNDLE_LOADER = "$(TEST_HOST)"; 581 | COPY_PHASE_STRIP = NO; 582 | FRAMEWORK_SEARCH_PATHS = ( 583 | "$(SDKROOT)/Developer/Library/Frameworks", 584 | "$(inherited)", 585 | ); 586 | INFOPLIST_FILE = StorekeeperAppTests/Info.plist; 587 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 588 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 589 | PRODUCT_NAME = "$(TARGET_NAME)"; 590 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/StorekeeperApp.app/StorekeeperApp"; 591 | }; 592 | name = Release; 593 | }; 594 | 13B07F941A680F5B00A75B9A /* Debug */ = { 595 | isa = XCBuildConfiguration; 596 | buildSettings = { 597 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 598 | HEADER_SEARCH_PATHS = ( 599 | "$(inherited)", 600 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 601 | "$(SRCROOT)/node_modules/react-native/React/**", 602 | ); 603 | INFOPLIST_FILE = "iOS/Info.plist"; 604 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 605 | OTHER_LDFLAGS = "-ObjC"; 606 | PRODUCT_NAME = StorekeeperApp; 607 | }; 608 | name = Debug; 609 | }; 610 | 13B07F951A680F5B00A75B9A /* Release */ = { 611 | isa = XCBuildConfiguration; 612 | buildSettings = { 613 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 614 | HEADER_SEARCH_PATHS = ( 615 | "$(inherited)", 616 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 617 | "$(SRCROOT)/node_modules/react-native/React/**", 618 | ); 619 | INFOPLIST_FILE = "iOS/Info.plist"; 620 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 621 | OTHER_LDFLAGS = "-ObjC"; 622 | PRODUCT_NAME = StorekeeperApp; 623 | }; 624 | name = Release; 625 | }; 626 | 83CBBA201A601CBA00E9B192 /* Debug */ = { 627 | isa = XCBuildConfiguration; 628 | buildSettings = { 629 | ALWAYS_SEARCH_USER_PATHS = NO; 630 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 631 | CLANG_CXX_LIBRARY = "libc++"; 632 | CLANG_ENABLE_MODULES = YES; 633 | CLANG_ENABLE_OBJC_ARC = YES; 634 | CLANG_WARN_BOOL_CONVERSION = YES; 635 | CLANG_WARN_CONSTANT_CONVERSION = YES; 636 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 637 | CLANG_WARN_EMPTY_BODY = YES; 638 | CLANG_WARN_ENUM_CONVERSION = YES; 639 | CLANG_WARN_INT_CONVERSION = YES; 640 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 641 | CLANG_WARN_UNREACHABLE_CODE = YES; 642 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 643 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 644 | COPY_PHASE_STRIP = NO; 645 | ENABLE_STRICT_OBJC_MSGSEND = YES; 646 | GCC_C_LANGUAGE_STANDARD = gnu99; 647 | GCC_DYNAMIC_NO_PIC = NO; 648 | GCC_OPTIMIZATION_LEVEL = 0; 649 | GCC_PREPROCESSOR_DEFINITIONS = ( 650 | "DEBUG=1", 651 | "$(inherited)", 652 | ); 653 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 654 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 655 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 656 | GCC_WARN_UNDECLARED_SELECTOR = YES; 657 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 658 | GCC_WARN_UNUSED_FUNCTION = YES; 659 | GCC_WARN_UNUSED_VARIABLE = YES; 660 | HEADER_SEARCH_PATHS = ( 661 | "$(inherited)", 662 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 663 | "$(SRCROOT)/node_modules/react-native/React/**", 664 | ); 665 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 666 | MTL_ENABLE_DEBUG_INFO = YES; 667 | ONLY_ACTIVE_ARCH = YES; 668 | SDKROOT = iphoneos; 669 | }; 670 | name = Debug; 671 | }; 672 | 83CBBA211A601CBA00E9B192 /* Release */ = { 673 | isa = XCBuildConfiguration; 674 | buildSettings = { 675 | ALWAYS_SEARCH_USER_PATHS = NO; 676 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 677 | CLANG_CXX_LIBRARY = "libc++"; 678 | CLANG_ENABLE_MODULES = YES; 679 | CLANG_ENABLE_OBJC_ARC = YES; 680 | CLANG_WARN_BOOL_CONVERSION = YES; 681 | CLANG_WARN_CONSTANT_CONVERSION = YES; 682 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 683 | CLANG_WARN_EMPTY_BODY = YES; 684 | CLANG_WARN_ENUM_CONVERSION = YES; 685 | CLANG_WARN_INT_CONVERSION = YES; 686 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 687 | CLANG_WARN_UNREACHABLE_CODE = YES; 688 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 689 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 690 | COPY_PHASE_STRIP = YES; 691 | ENABLE_NS_ASSERTIONS = NO; 692 | ENABLE_STRICT_OBJC_MSGSEND = YES; 693 | GCC_C_LANGUAGE_STANDARD = gnu99; 694 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 695 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 696 | GCC_WARN_UNDECLARED_SELECTOR = YES; 697 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 698 | GCC_WARN_UNUSED_FUNCTION = YES; 699 | GCC_WARN_UNUSED_VARIABLE = YES; 700 | HEADER_SEARCH_PATHS = ( 701 | "$(inherited)", 702 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 703 | "$(SRCROOT)/node_modules/react-native/React/**", 704 | ); 705 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 706 | MTL_ENABLE_DEBUG_INFO = NO; 707 | SDKROOT = iphoneos; 708 | VALIDATE_PRODUCT = YES; 709 | }; 710 | name = Release; 711 | }; 712 | /* End XCBuildConfiguration section */ 713 | 714 | /* Begin XCConfigurationList section */ 715 | 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "StorekeeperAppTests" */ = { 716 | isa = XCConfigurationList; 717 | buildConfigurations = ( 718 | 00E356F61AD99517003FC87E /* Debug */, 719 | 00E356F71AD99517003FC87E /* Release */, 720 | ); 721 | defaultConfigurationIsVisible = 0; 722 | defaultConfigurationName = Release; 723 | }; 724 | 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "StorekeeperApp" */ = { 725 | isa = XCConfigurationList; 726 | buildConfigurations = ( 727 | 13B07F941A680F5B00A75B9A /* Debug */, 728 | 13B07F951A680F5B00A75B9A /* Release */, 729 | ); 730 | defaultConfigurationIsVisible = 0; 731 | defaultConfigurationName = Release; 732 | }; 733 | 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "StorekeeperApp" */ = { 734 | isa = XCConfigurationList; 735 | buildConfigurations = ( 736 | 83CBBA201A601CBA00E9B192 /* Debug */, 737 | 83CBBA211A601CBA00E9B192 /* Release */, 738 | ); 739 | defaultConfigurationIsVisible = 0; 740 | defaultConfigurationName = Release; 741 | }; 742 | /* End XCConfigurationList section */ 743 | }; 744 | rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; 745 | } 746 | -------------------------------------------------------------------------------- /StorekeeperApp/StorekeeperApp.xcodeproj/xcshareddata/xcschemes/StorekeeperApp.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 | -------------------------------------------------------------------------------- /StorekeeperApp/StorekeeperAppTests/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 | -------------------------------------------------------------------------------- /StorekeeperApp/StorekeeperAppTests/StorekeeperAppTests.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 StorekeeperAppTests : XCTestCase 21 | 22 | @end 23 | 24 | @implementation StorekeeperAppTests 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 | -------------------------------------------------------------------------------- /StorekeeperApp/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 | -------------------------------------------------------------------------------- /StorekeeperApp/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:@"StorekeeperApp" 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 | -------------------------------------------------------------------------------- /StorekeeperApp/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 | -------------------------------------------------------------------------------- /StorekeeperApp/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 | } -------------------------------------------------------------------------------- /StorekeeperApp/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 | -------------------------------------------------------------------------------- /StorekeeperApp/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 | -------------------------------------------------------------------------------- /StorekeeperApp/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 | -------------------------------------------------------------------------------- /StorekeeperApp/index.ios.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var LatestOrders = require('./src/components/latest_orders'); 3 | var { 4 | AppRegistry, 5 | StyleSheet, 6 | Text, 7 | View, 8 | } = React; 9 | 10 | var StorekeeperApp = React.createClass({ 11 | render: function() { 12 | return ( 13 | 14 | 15 | Storekeeper 16 | 17 | 18 | 19 | ); 20 | } 21 | }); 22 | 23 | var styles = StyleSheet.create({ 24 | container: { 25 | flex: 1, 26 | backgroundColor: '#F5FCFF', 27 | }, 28 | welcome: { 29 | fontSize: 30, 30 | textAlign: 'center', 31 | margin: 40, 32 | }, 33 | }); 34 | 35 | AppRegistry.registerComponent('StorekeeperApp', () => StorekeeperApp); 36 | -------------------------------------------------------------------------------- /StorekeeperApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "StorekeeperApp", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node_modules/react-native/packager/packager.sh" 7 | }, 8 | "dependencies": { 9 | "alt": "^0.17.1", 10 | "immutable": "^3.7.4", 11 | "moment": "^2.10.6", 12 | "numeral": "^1.5.3", 13 | "react-native": "^0.9.0", 14 | "underscore": "^1.8.3" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /StorekeeperApp/src/actions/orders_actions.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt'); 2 | var OrdersFetcher = require('../utils/orders_fetcher'); 3 | 4 | class OrdersActions { 5 | updateOrders(orders) { 6 | this.dispatch(orders); 7 | } 8 | 9 | fetchOrders() { 10 | OrdersFetcher.fetch() 11 | .then(this.actions.updateOrders.bind(this)); 12 | } 13 | } 14 | 15 | module.exports = alt.createActions(OrdersActions); 16 | -------------------------------------------------------------------------------- /StorekeeperApp/src/alt.js: -------------------------------------------------------------------------------- 1 | var Alt = require('alt'); 2 | module.exports = new Alt(); 3 | -------------------------------------------------------------------------------- /StorekeeperApp/src/components/latest_orders.js: -------------------------------------------------------------------------------- 1 | var React = require('react-native'); 2 | var OrdersStore = require('../stores/orders_store'); 3 | var OrdersActions = require('../actions/orders_actions'); 4 | var { toMoney, toDateTime } = require('../lib/formatters'); 5 | 6 | var { StyleSheet, Text, View } = React; 7 | 8 | class LatestOrders extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.onChange = this.onChange.bind(this); 12 | this.state = OrdersStore.getState(); 13 | } 14 | 15 | componentDidMount() { 16 | OrdersStore.listen(this.onChange); 17 | OrdersActions.fetchOrders(); 18 | } 19 | 20 | componentWillUnmount() { 21 | OrdersStore.unlisten(this.onChange); 22 | } 23 | 24 | onChange(state) { 25 | this.setState(state); 26 | } 27 | 28 | render() { 29 | var rows = this.state.orders.map((order, i) => { 30 | return ( 31 | 32 | {order.get('reference')} 33 | {toDateTime(order.get('orderedAt'))} 34 | {toMoney(order.get('amount'))} 35 | 36 | ); 37 | }); 38 | 39 | return ( 40 | 41 | Latest Orders 42 | 43 | {rows} 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | var styles = StyleSheet.create({ 51 | title: { 52 | fontSize: 24, 53 | alignSelf: 'center', 54 | marginBottom: 30 55 | }, 56 | 57 | row: { 58 | flexDirection: 'row', 59 | justifyContent: 'space-between', 60 | paddingHorizontal: 10, 61 | paddingVertical: 5 62 | }, 63 | 64 | column: { 65 | fontSize: 18 66 | }, 67 | 68 | ref: { 69 | width: 80 70 | }, 71 | 72 | date: { 73 | width: 150, 74 | textAlign: 'center' 75 | }, 76 | 77 | amount: { 78 | width: 80, 79 | textAlign: 'right' 80 | } 81 | }); 82 | 83 | module.exports = LatestOrders; 84 | -------------------------------------------------------------------------------- /StorekeeperApp/src/lib/formatters.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | var numeral = require('numeral'); 3 | var moment = require('moment'); 4 | var Moment = moment().constructor; 5 | 6 | function toMoney(num: number) : string { 7 | return numeral(num).format('$0,0.00'); 8 | }; 9 | 10 | function toDateTime(mom: Moment) : string { 11 | return mom.format('MMM D, h:mma'); 12 | }; 13 | 14 | function toTitleCase(str: string) : string { 15 | return str.replace(/\w\S*/g, (txt) => { 16 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 17 | }); 18 | }; 19 | 20 | module.exports = { toMoney, toDateTime, toTitleCase }; 21 | -------------------------------------------------------------------------------- /StorekeeperApp/src/stores/orders_store.js: -------------------------------------------------------------------------------- 1 | var alt = require('../alt'); 2 | var Immutable = require('immutable'); 3 | var OrdersActions = require('../actions/orders_actions'); 4 | 5 | class OrdersStore { 6 | constructor() { 7 | this.state = { 8 | orders: Immutable.List(), 9 | selectedStatus: 'all', 10 | amountFilter: null 11 | } 12 | 13 | this.bindListeners({ 14 | handleUpdateOrders: OrdersActions.UPDATE_ORDERS 15 | }); 16 | } 17 | 18 | handleUpdateOrders(orders) { 19 | this.setState({ orders: orders }); 20 | } 21 | } 22 | 23 | module.exports = alt.createStore(OrdersStore, 'OrdersStore'); 24 | -------------------------------------------------------------------------------- /StorekeeperApp/src/utils/orders_fetcher.js: -------------------------------------------------------------------------------- 1 | var Immutable = require('immutable'); 2 | var parseOrders = require('./parse_orders'); 3 | 4 | var OrdersFetcher = { 5 | fetch() { 6 | return fetch('http://localhost:8000/orders.json') 7 | .then((resp) => resp.json()) 8 | .then(parseOrders) 9 | .then((orders) => Immutable.fromJS(orders)); 10 | } 11 | }; 12 | 13 | module.exports = OrdersFetcher; 14 | -------------------------------------------------------------------------------- /StorekeeperApp/src/utils/parse_orders.js: -------------------------------------------------------------------------------- 1 | var moment = require('moment'); 2 | 3 | function parseOrders(rawOrders) { 4 | var orders = rawOrders.map((order) => { 5 | return Object.assign( 6 | {}, 7 | order, 8 | { 9 | orderedAt: moment(order.orderedAt), 10 | products: order.products.map((product) => { 11 | return Object.assign( 12 | {}, 13 | product, 14 | { 15 | amount: parseFloat(product.amount) 16 | } 17 | ); 18 | }), 19 | amount: parseFloat(order.amount) 20 | } 21 | ); 22 | }); 23 | 24 | return orders; 25 | }; 26 | 27 | module.exports = parseOrders; 28 | -------------------------------------------------------------------------------- /data/orders.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "reference": "ASX13X", 4 | "customer": "Bruce Wayne", 5 | "orderedAt": "2015-03-09T17:14:00.000Z", 6 | "products": [ 7 | { "name": "Blue Wool Shirt", "amount": "120.25" }, 8 | { "name": "Pearl White Shirt", "amount": "220.50" } 9 | ], 10 | "amount": "340.75", 11 | "paymentStatus": "authorized", 12 | "orderStatus": "open" 13 | }, 14 | { 15 | "reference": "KCA31F", 16 | "customer": "Selina Kyle", 17 | "orderedAt": "2015-03-09T17:08:00.000Z", 18 | "products": [ 19 | { "name": "Black Dress", "amount": "29.75" } 20 | ], 21 | "amount": "29.75", 22 | "paymentStatus": "authorized", 23 | "orderStatus": "open" 24 | }, 25 | { 26 | "reference": "MCZ96G", 27 | "customer": "Bruce Banner", 28 | "orderedAt": "2015-03-09T16:17:00.000Z", 29 | "products": [ 30 | { "name": "Purple Shorts", "amount": "5.50" } 31 | ], 32 | "amount": "5.50", 33 | "paymentStatus": "settled", 34 | "orderStatus": "open" 35 | }, 36 | { 37 | "reference": "LPX77Q", 38 | "customer": "Walter Kovacs", 39 | "orderedAt": "2015-03-09T15:49:00.000Z", 40 | "products": [ 41 | { "name": "Fedora", "amount": "65.50" }, 42 | { "name": "Brown Trenchcoat", "amount": "250.50" } 43 | ], 44 | "amount": "316.00", 45 | "paymentStatus": "settled", 46 | "orderStatus": "shipped" 47 | } 48 | ] 49 | -------------------------------------------------------------------------------- /data/sales_stats.json: -------------------------------------------------------------------------------- 1 | [ 2 | "420432", 3 | "392540", 4 | "352001", 5 | "360412", 6 | "414905", 7 | "421525", 8 | "385969", 9 | "510463", 10 | "460210", 11 | "501553", 12 | "499210", 13 | "420431" 14 | ] 15 | -------------------------------------------------------------------------------- /digest_email/digest_email.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SalesSparkline from './sales_sparkline'; 3 | 4 | class DigestEmail extends React.Component { 5 | render() { 6 | return ( 7 | 8 | 9 |

Storekeeper Digest Email

10 | 11 | 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default DigestEmail; 18 | -------------------------------------------------------------------------------- /digest_email/render.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import DigestEmail from './digest_email'; 4 | import fs from 'fs'; 5 | import juice from 'juice'; 6 | import Immutable from 'immutable'; 7 | import { jsdom } from 'jsdom'; 8 | import { exec } from 'child_process'; 9 | 10 | function render(callback) { 11 | const salesStats = Immutable.fromJS( 12 | JSON.parse( 13 | fs.readFileSync(__dirname + '/../data/sales_stats.json', { encoding: 'utf8' }) 14 | ).map(parseFloat) 15 | ); 16 | 17 | const css = fs.readFileSync(__dirname + '/styles.css', { encoding: 'utf8' }); 18 | const html = juice.inlineContent( 19 | ReactDOMServer.renderToStaticMarkup(), 20 | css 21 | ); 22 | 23 | const document = jsdom(html); 24 | const svg = document.querySelector('svg'); 25 | 26 | let pngData; 27 | exec(`echo '${svg.outerHTML}' | rsvg-convert | base64`, (err, stdout, stderr) => { 28 | pngData = stdout; 29 | 30 | let img = document.createElement('img'); 31 | img.src = 'cid:salessparkline'; 32 | svg.parentNode.replaceChild(img, svg); 33 | 34 | callback(document.querySelector('html').outerHTML, [ 35 | { 36 | type: 'image/png', 37 | name: 'salessparkline', 38 | content: pngData, 39 | } 40 | ]); 41 | }); 42 | } 43 | 44 | export default render; 45 | -------------------------------------------------------------------------------- /digest_email/sales_sparkline.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import d3 from 'd3'; 3 | import { max } from 'underscore'; 4 | import Immutable from 'immutable'; 5 | 6 | const CHART_WIDTH = 300; 7 | const CHART_HEIGHT = 150; 8 | 9 | class SalesSparkline extends React.Component { 10 | render() { 11 | const salesStats = this.props.salesStats.toJS(); 12 | const numDataPoints = salesStats.length; 13 | 14 | const maxSales = max(salesStats); 15 | 16 | const xScale = d3.scale.linear() 17 | .domain([0, numDataPoints]) 18 | .range([0, CHART_WIDTH]); 19 | 20 | const yScale = d3.scale.linear() 21 | .domain([0, maxSales]) 22 | .range([CHART_HEIGHT, 0]); 23 | 24 | const line = d3.svg.line() 25 | .x((d,i) => xScale(i)) 26 | .y((d) => yScale(d)) 27 | .interpolate('linear'); 28 | 29 | const chartStyles = { 30 | width: CHART_WIDTH, 31 | height: CHART_HEIGHT, 32 | fill: '#ffffff', 33 | }; 34 | 35 | const lineStyles = { 36 | stroke: '#FF5722', 37 | fill: 'none', 38 | }; 39 | 40 | return ( 41 |
42 | 43 | 44 | 45 |
46 | ); 47 | } 48 | } 49 | 50 | export default SalesSparkline; 51 | -------------------------------------------------------------------------------- /digest_email/send.js: -------------------------------------------------------------------------------- 1 | import mandrill from 'mandrill-api/mandrill'; 2 | import render from './render'; 3 | 4 | const mandrillClient = new mandrill.Mandrill(process.env.STOREKEEPER_MANDRIL_KEY); 5 | 6 | render((html, images) => { 7 | const message = { 8 | "html": html, 9 | "subject": "Storekeeper Digest", 10 | "from_email": "r.hackr+storekeeper@gmail.com", 11 | "from_name": "Storekeeper", 12 | "to": [{ 13 | "email": "r.hackr+storekeeper@gmail.com", 14 | "name": "Simon Hoejberg", 15 | "type": "to" 16 | }], 17 | 'images': images 18 | }; 19 | 20 | mandrillClient.messages.send({ message: message }, (result) => { 21 | console.log("Email sent"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /digest_email/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | text-align: center; 3 | font-family: Arial; 4 | color: #42474E; 5 | padding-top: 50px; 6 | } 7 | 8 | .sales-sparkline { 9 | width: 300px; 10 | margin: auto; 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mastering-react", 3 | "version": "1.0.0", 4 | "description": "Mastering React video course", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "clear && ./node_modules/mocha/bin/mocha --compilers js:babel/register --reporter dot --recursive", 8 | "build": "browserify src/client.js -t babelify --outfile public/bundle.js && node-sass --output-style compressed styles/main.scss public/main.css", 9 | "flow": "babel src --out-dir build_flow --blacklist flow && flow build_flow" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/hojberg/mastering-react.git" 14 | }, 15 | "author": "Simon Hoejberg", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/hojberg/mastering-react/issues" 19 | }, 20 | "homepage": "https://github.com/hojberg/mastering-react", 21 | "dependencies": { 22 | "alt": "^0.17.1", 23 | "d3": "^3.5.6", 24 | "ejs": "^2.3.3", 25 | "express": "^4.13.1", 26 | "immutable": "^3.7.4", 27 | "iso": "^4.1.0", 28 | "juice": "^1.3.4", 29 | "mandrill-api": "^1.0.45", 30 | "moment": "^2.10.3", 31 | "numeral": "^1.5.3", 32 | "react": "^0.14.0-beta1", 33 | "react-dom": "^0.14.0-beta1", 34 | "react-router": "^1.0.0-beta3", 35 | "underscore": "^1.8.3" 36 | }, 37 | "devDependencies": { 38 | "babel": "^5.6.14", 39 | "babel-plugin-flow-comments": "^1.0.4", 40 | "babelify": "^6.1.2", 41 | "expect.js": "^0.3.1", 42 | "jsdom": "^3.1.2", 43 | "mocha": "^2.2.5", 44 | "node-sass": "^3.2.0", 45 | "sinon": "^1.15.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fs from 'fs'; 3 | import { Router } from 'react-router'; 4 | import Location from 'react-router/lib/location'; 5 | import routes from './src/routes'; 6 | import ReactDOMServer from 'react-dom/server'; 7 | import React from 'react'; 8 | import Iso from 'iso'; 9 | import alt from './src/alt'; 10 | 11 | let app = express(); 12 | app.use(express.static('public')); 13 | 14 | // Data endpoints 15 | app.get('/orders.json', (req, res) => { 16 | res.sendFile(__dirname + '/data/orders.json'); 17 | }); 18 | 19 | app.get('/sales_stats.json', (req, res) => { 20 | res.sendFile(__dirname + '/data/sales_stats.json'); 21 | }); 22 | 23 | app.get('/orders', (req, res, next) => { 24 | const orders = JSON.parse(fs.readFileSync(__dirname + '/data/orders.json')); 25 | 26 | res.locals.data = { 27 | OrdersStore: { 28 | orders: orders, 29 | selectedStatus: 'all', 30 | amountFilter: null 31 | } 32 | }; 33 | 34 | next(); 35 | }); 36 | 37 | // Render UI 38 | app.use((req, res, next) => { 39 | alt.bootstrap(JSON.stringify(res.locals.data || {})); 40 | let iso = new Iso(); 41 | 42 | Router.run(routes, new Location(req.url), (error, props) => { 43 | const content = ReactDOMServer.renderToString(); 44 | iso.add(content, alt.flush()); 45 | res.render('index.ejs', { html: iso.render() }); 46 | }); 47 | }); 48 | 49 | const server = app.listen(8000, () => { 50 | const host = server.address().address; 51 | const port = server.address().port; 52 | 53 | console.log('Storekeeper app listening at http://%s:%s', host, port); 54 | }); 55 | 56 | -------------------------------------------------------------------------------- /src/actions/dashboard_actions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import SalesStatsFetcher from '../utils/sales_stats_fetcher'; 3 | 4 | class DashboardActions { 5 | updateSalesStats(stats) { 6 | this.dispatch(stats); 7 | } 8 | 9 | fetchSalesStats() { 10 | this.dispatch([]); 11 | SalesStatsFetcher.fetch() 12 | .then(this.actions.updateSalesStats.bind(this)); 13 | } 14 | } 15 | 16 | export default alt.createActions(DashboardActions); 17 | -------------------------------------------------------------------------------- /src/actions/orders_actions.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import OrdersFetcher from '../utils/orders_fetcher'; 3 | 4 | class OrdersActions { 5 | updateOrders(orders) { 6 | this.dispatch(orders); 7 | } 8 | 9 | updateSelectedStatus(status) { 10 | this.dispatch(status); 11 | } 12 | 13 | updateAmountFilter(amount) { 14 | this.dispatch(amount); 15 | } 16 | 17 | fetchOrders() { 18 | OrdersFetcher.fetch() 19 | .then(this.actions.updateOrders.bind(this)); 20 | } 21 | } 22 | 23 | export default alt.createActions(OrdersActions); 24 | -------------------------------------------------------------------------------- /src/alt.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt'; 2 | export default new Alt(); 3 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | require("babel/polyfill"); 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Router } from 'react-router'; 6 | import { history } from 'react-router/lib/BrowserHistory'; 7 | import routes from './routes'; 8 | import Iso from 'iso'; 9 | import alt from './alt'; 10 | 11 | Iso.bootstrap((state, _, container) => { 12 | alt.bootstrap(state); 13 | 14 | ReactDOM.render(( 15 | 16 | {routes} 17 | 18 | ), container); 19 | }); 20 | -------------------------------------------------------------------------------- /src/components/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainHeader from './main_header'; 3 | import Page from './page'; 4 | 5 | class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | this.state = { i: 0 }; 9 | } 10 | 11 | render() { 12 | return ( 13 |
14 | 15 | 16 | {this.props.children} 17 | 18 |
19 | ); 20 | } 21 | 22 | componentDidMount() { 23 | this.increment(); 24 | } 25 | 26 | componentDidUpdate() { 27 | setTimeout(this.increment.bind(this), 200); 28 | } 29 | 30 | increment() { 31 | this.setState({ i: this.state.i++ }); 32 | } 33 | } 34 | 35 | export default App; 36 | -------------------------------------------------------------------------------- /src/components/dashboard.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DashboardStore from '../stores/dashboard_store'; 3 | import DashboardActions from '../actions/dashboard_actions'; 4 | import SalesChart from './sales_chart'; 5 | 6 | class Dashboard extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.onChange = this.onChange.bind(this); 10 | this.state = DashboardStore.getState(); 11 | } 12 | 13 | componentDidMount() { 14 | DashboardStore.listen(this.onChange); 15 | DashboardActions.fetchSalesStats(); 16 | } 17 | 18 | componentWillUnmount() { 19 | DashboardStore.unlisten(this.onChange); 20 | } 21 | 22 | onChange(state) { 23 | this.setState(state); 24 | } 25 | 26 | render() { 27 | return ( 28 |
29 |

Dashboard

30 | 31 |
32 | ); 33 | } 34 | } 35 | 36 | export default Dashboard; 37 | -------------------------------------------------------------------------------- /src/components/main_header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainNav from './main_nav'; 3 | 4 | class MainHeader extends React.Component { 5 | render() { 6 | return ( 7 |
8 |
Storekeeper
9 | 10 |
11 | ); 12 | } 13 | } 14 | 15 | export default MainHeader; 16 | -------------------------------------------------------------------------------- /src/components/main_nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | 4 | class MainNav extends React.Component { 5 | render() { 6 | return ( 7 | 12 | ); 13 | } 14 | } 15 | 16 | export default MainNav; 17 | -------------------------------------------------------------------------------- /src/components/not_found.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class NotFound extends React.Component { 4 | render() { 5 | return
Not Found
; 6 | } 7 | } 8 | 9 | export default NotFound; 10 | -------------------------------------------------------------------------------- /src/components/orders.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from './page_header'; 3 | import OrdersTable from './orders/orders_table'; 4 | import { toTitleCase } from '../lib/formatters'; 5 | import OrdersStore from '../stores/orders_store'; 6 | import OrdersActions from '../actions/orders_actions'; 7 | 8 | const STATUSES = ['all', 'open', 'shipped']; 9 | 10 | class TopCustomer extends React.Component { 11 | render() { 12 | let { topOrder } = this.props; 13 | if (!topOrder) return null; 14 | 15 | topOrder = topOrder.set('customer', topOrder.get('customer').split(' ')[0]); 16 | 17 | return
Top customer: {topOrder.get('customer')}
; 18 | } 19 | } 20 | 21 | class Orders extends React.Component { 22 | constructor(props) { 23 | super(props); 24 | 25 | this.onChange = this.onChange.bind(this); 26 | 27 | this.state = OrdersStore.getState(); 28 | } 29 | 30 | componentDidMount() { 31 | OrdersStore.listen(this.onChange); 32 | OrdersActions.fetchOrders(); 33 | } 34 | 35 | componentWillUnmount() { 36 | OrdersStore.unlisten(this.onChange); 37 | } 38 | 39 | onChange(state) { 40 | this.setState(state); 41 | } 42 | 43 | render() { 44 | const { selectedStatus, amountFilter } = this.state; 45 | 46 | const statuses = STATUSES.map((status, i) => { 47 | const className = status === selectedStatus ? 'selected status' : 'status'; 48 | return ( 49 | 50 | {toTitleCase(status)} 51 | 52 | ); 53 | }); 54 | 55 | let orders = this.state.orders; 56 | if (selectedStatus !== 'all') { 57 | orders = orders.filter((order) => { 58 | return order.get('orderStatus') === selectedStatus; 59 | }); 60 | } 61 | 62 | if (amountFilter) { 63 | orders = orders.filter((o) => o.get('amount') === parseFloat(amountFilter)); 64 | } 65 | 66 | const topOrder = orders.size ? orders.get(0) : null; 67 | 68 | return ( 69 |
70 | 71 |

Orders

72 | 73 |
74 | 79 |
80 |
81 | 82 | 83 |
84 | ); 85 | } 86 | 87 | shouldComponentUpdate(nextProps, nextState) { 88 | return this.state !== nextState; 89 | } 90 | 91 | handleAmountFilterChange(ev) { 92 | OrdersActions.updateAmountFilter(ev.currentTarget.value || null); 93 | } 94 | 95 | handleStatusClick(status) { 96 | OrdersActions.updateSelectedStatus(status); 97 | } 98 | } 99 | 100 | export default Orders; 101 | -------------------------------------------------------------------------------- /src/components/orders/order_row.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { toMoney, toDateTime, toTitleCase } from '../../lib/formatters'; 3 | 4 | class OrderRow extends React.Component { 5 | render() { 6 | const { order } = this.props; 7 | const products = order.get('products').map((p) => p.get('name')).join(', '); 8 | 9 | return ( 10 | 11 | {order.get('reference')} 12 | {order.get('customer')} 13 | {toDateTime(order.get('orderedAt'))} 14 | {products} 15 | {toMoney(order.get('amount'))} 16 | {toTitleCase(order.get('paymentStatus'))} 17 | {toTitleCase(order.get('orderStatus'))} 18 | 19 | ); 20 | } 21 | 22 | shouldComponentUpdate(nextProps, nextState) { 23 | return nextProps.order !== this.props.order; 24 | } 25 | } 26 | 27 | export default OrderRow; 28 | -------------------------------------------------------------------------------- /src/components/orders/orders_table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import OrderRow from './order_row'; 3 | 4 | class OrdersTable extends React.Component { 5 | render() { 6 | const rows = this.props.orders.map((order, i) => { 7 | return ; 8 | }); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {rows} 25 | 26 |
Order #CustomerOrdered atProduct(s)AmountPayment statusOrder status
27 | ); 28 | } 29 | } 30 | 31 | export default OrdersTable; 32 | -------------------------------------------------------------------------------- /src/components/page.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Page extends React.Component { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default Page; 14 | -------------------------------------------------------------------------------- /src/components/page_header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class PageHeader extends React.Component { 4 | render() { 5 | return ( 6 |
7 | {this.props.children} 8 |
9 | ); 10 | } 11 | } 12 | 13 | export default PageHeader 14 | -------------------------------------------------------------------------------- /src/components/sales_chart.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import d3 from 'd3'; 3 | import { max } from 'underscore'; 4 | import Immutable from 'immutable'; 5 | import SalesChartXAxis from './sales_chart_x_axis'; 6 | import SalesChartYAxis from './sales_chart_y_axis'; 7 | 8 | const CHART_WIDTH = 1100; 9 | const CHART_HEIGHT = 250; 10 | const CHART_PADDING = 20; 11 | const LEFT_MARGIN = 45; 12 | const BOTTOM_MARGIN = 20; 13 | 14 | class SalesChart extends React.Component { 15 | render() { 16 | const salesStats = this.props.salesStats.toJS(); 17 | const numDataPoints = salesStats.length; 18 | 19 | if (!numDataPoints) return null; 20 | 21 | const maxSales = max(salesStats) * 1.10; 22 | 23 | const xScale = d3.scale.linear() 24 | .domain([0, numDataPoints]) 25 | .range([0 + CHART_PADDING, CHART_WIDTH - CHART_PADDING]); 26 | 27 | const yScale = d3.scale.linear() 28 | .domain([0, maxSales]) 29 | .range([CHART_HEIGHT - CHART_PADDING, 0 + CHART_PADDING]); 30 | 31 | const line = d3.svg.line() 32 | .x((d,i) => xScale(i) + LEFT_MARGIN + 10) 33 | .y((d) => yScale(d)) 34 | .interpolate('linear'); 35 | 36 | const boxStyles = { width: CHART_WIDTH, height: CHART_HEIGHT }; 37 | 38 | return ( 39 |
40 |
41 | 42 | 43 | 50 | 56 | 57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | export default SalesChart; 64 | -------------------------------------------------------------------------------- /src/components/sales_chart_x_axis.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import { range } from 'underscore'; 4 | 5 | function getXTicks(num) { 6 | const today = moment(); 7 | const earliest = today.clone().subtract(num, 'months'); 8 | return range(num).map((n) => { 9 | return earliest.clone().add(n, 'months').format("MMM"); 10 | }); 11 | } 12 | 13 | class SalesChartXAxis extends React.Component { 14 | render() { 15 | const { xScale, numDataPoints, width, height, leftMargin, bottomMargin } = this.props; 16 | 17 | const xTicks = getXTicks(numDataPoints).map((tick, i) => { 18 | const x = xScale(i) + leftMargin; 19 | return {tick}; 20 | }); 21 | 22 | return ( 23 | 24 | {xTicks} 25 | 31 | 32 | ); 33 | } 34 | } 35 | 36 | export default SalesChartXAxis; 37 | -------------------------------------------------------------------------------- /src/components/sales_chart_y_axis.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import numeral from 'numeral'; 3 | 4 | function formatYTick(tick) { 5 | return numeral(tick).format('$0a'); 6 | } 7 | 8 | class SalesChartYAxis extends React.Component { 9 | render() { 10 | const { yScale, maxSales, height, leftMargin, bottomMargin } = this.props; 11 | 12 | const yTicks = [maxSales, maxSales/2, 0].map((tick, i) => { 13 | const y = yScale(tick); 14 | return {formatYTick(tick)}; 15 | }); 16 | return ( 17 | 18 | {yTicks} 19 | 25 | 26 | ); 27 | } 28 | } 29 | 30 | export default SalesChartYAxis; 31 | -------------------------------------------------------------------------------- /src/data/orders.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | const ORDERS_DATA = [ 4 | { 5 | reference: 'ASX13X', 6 | customer: 'Bruce Wayne', 7 | orderedAt: moment("2015-03-09T17:14:00.000Z"), 8 | products: [ 9 | { name: 'Blue Wool Shirt', amount: 120.25 }, 10 | { name: 'Pearl White Shirt', amount: 220.50 } 11 | ], 12 | amount: 340.75, 13 | paymentStatus: 'authorized', 14 | orderStatus: 'open' 15 | }, 16 | { 17 | reference: 'KCA31F', 18 | customer: 'Selina Kyle', 19 | orderedAt: moment("2015-03-09T17:08:00.000Z"), 20 | products: [ 21 | { name: 'Black Dress', amount: 29.75 } 22 | ], 23 | amount: 29.75, 24 | paymentStatus: 'authorized', 25 | orderStatus: 'open' 26 | }, 27 | { 28 | reference: 'MCZ96G', 29 | customer: 'Bruce Banner', 30 | orderedAt: moment("2015-03-09T16:17:00.000Z"), 31 | products: [ 32 | { name: 'Purple Shorts', amount: 5.50 } 33 | ], 34 | amount: 5.50, 35 | paymentStatus: 'settled', 36 | orderStatus: 'open' 37 | }, 38 | { 39 | reference: 'LPX77Q', 40 | customer: 'Walter Kovacs', 41 | orderedAt: moment("2015-03-09T15:49:00.000Z"), 42 | products: [ 43 | { name: 'Fedora', amount: 65.50 }, 44 | { name: 'Brown Trenchcoat', amount: 250.50 } 45 | ], 46 | amount: 316.00, 47 | paymentStatus: 'settled', 48 | orderStatus: 'shipped' 49 | } 50 | ]; 51 | 52 | export default ORDERS_DATA; 53 | -------------------------------------------------------------------------------- /src/lib/formatters.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import numeral from 'numeral'; 3 | import moment from 'moment'; 4 | const Moment = moment().constructor; 5 | 6 | const toMoney = function (num: number) : string { 7 | return numeral(num).format('$0,0.00'); 8 | }; 9 | 10 | const toDateTime = function (mom: Moment) : string { 11 | return mom.format('MMM D, h:mma'); 12 | }; 13 | 14 | const toTitleCase = function (str: string) : string { 15 | return str.replace(/\w\S*/g, (txt) => { 16 | return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); 17 | }); 18 | }; 19 | 20 | export { toMoney, toDateTime, toTitleCase }; 21 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router'; 3 | 4 | import App from './components/app'; 5 | import Dashboard from './components/dashboard'; 6 | import Orders from './components/orders'; 7 | import NotFound from './components/not_found'; 8 | 9 | const routes = ( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | export default routes; 18 | -------------------------------------------------------------------------------- /src/stores/dashboard_store.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import immutableUtil from 'alt/utils/ImmutableUtil'; 3 | import Immutable from 'immutable'; 4 | import DashboardActions from '../actions/dashboard_actions'; 5 | 6 | class DashboardStore { 7 | constructor() { 8 | this.state = Immutable.fromJS({ 9 | salesStats: [] 10 | }); 11 | 12 | this.bindListeners({ 13 | handleUpdateSalesStats: DashboardActions.UPDATE_SALES_STATS, 14 | handleFetchSalesStats: DashboardActions.FETCH_SALES_STATS 15 | }); 16 | } 17 | 18 | handleUpdateSalesStats(salesStats) { 19 | this.setState(this.state.setIn(['salesStates', salesStates])); 20 | } 21 | 22 | handleFetchSalesStats() { 23 | this.setState(this.state.setIn(['salesStates', Immutable.List()])); 24 | } 25 | } 26 | 27 | export default alt.createStore(immutableUtil(DashboardStore), 'DashboardStore'); 28 | -------------------------------------------------------------------------------- /src/stores/orders_store.js: -------------------------------------------------------------------------------- 1 | import alt from '../alt'; 2 | import Immutable from 'immutable'; 3 | import OrdersActions from '../actions/orders_actions'; 4 | import parseOrders from '../utils/parse_orders'; 5 | 6 | class OrdersStore { 7 | constructor() { 8 | this.state = { 9 | orders: Immutable.List(), 10 | selectedStatus: 'all', 11 | amountFilter: null 12 | } 13 | 14 | this.bindListeners({ 15 | handleUpdateOrders: OrdersActions.UPDATE_ORDERS, 16 | handleUpdateSelectedStatus: OrdersActions.UPDATE_SELECTED_STATUS, 17 | handleUpdateAmountFilter: OrdersActions.UPDATE_AMOUNT_FILTER, 18 | }); 19 | } 20 | 21 | handleUpdateOrders(orders) { 22 | this.setState({ orders: orders }); 23 | } 24 | 25 | handleUpdateAmountFilter(amount) { 26 | this.setState({ amountFilter: amount }); 27 | } 28 | 29 | handleUpdateSelectedStatus(status) { 30 | this.setState({ selectedStatus: status }); 31 | } 32 | } 33 | 34 | OrdersStore.config = { 35 | onDeserialize: function (data) { 36 | const nextState = Object.assign( 37 | {}, 38 | data, 39 | { 40 | orders: Immutable.fromJS(parseOrders(data.orders)) 41 | } 42 | ); 43 | 44 | return nextState; 45 | } 46 | }; 47 | 48 | export default alt.createStore(OrdersStore, 'OrdersStore'); 49 | -------------------------------------------------------------------------------- /src/utils/orders_fetcher.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import parseOrders from './parse_orders'; 3 | 4 | const OrdersFetcher = { 5 | fetch() { 6 | return fetch('/orders.json') 7 | .then((resp) => resp.json()) 8 | .then(parseOrders) 9 | .then((orders) => Immutable.fromJS(orders)); 10 | } 11 | }; 12 | 13 | export default OrdersFetcher; 14 | -------------------------------------------------------------------------------- /src/utils/parse_orders.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | function parseOrders(rawOrders) { 4 | const orders = rawOrders.map((order) => { 5 | return Object.assign( 6 | {}, 7 | order, 8 | { 9 | orderedAt: moment(order.orderedAt), 10 | products: order.products.map((product) => { 11 | return Object.assign( 12 | {}, 13 | product, 14 | { 15 | amount: parseFloat(product.amount) 16 | } 17 | ); 18 | }), 19 | amount: parseFloat(order.amount) 20 | } 21 | ); 22 | }); 23 | 24 | return orders; 25 | }; 26 | 27 | export default parseOrders; 28 | -------------------------------------------------------------------------------- /src/utils/sales_stats_fetcher.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | const SalesStatsFetcher = { 4 | fetch() { 5 | return fetch('/sales_stats.json') 6 | .then((resp) => resp.json()) 7 | .then((json) => json.map(parseFloat)) 8 | .then((salesStats) => Immutable.fromJS(salesStats)); 9 | } 10 | }; 11 | 12 | export default SalesStatsFetcher; 13 | -------------------------------------------------------------------------------- /styles/_base.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | font: 14px $font; 5 | background: $white; 6 | color: $grey-100; 7 | } 8 | 9 | h1, h2, h3, h4, h5 { 10 | margin: 0; 11 | padding: 0; 12 | font-weight: $regular; 13 | } 14 | 15 | h1 { 16 | font-size: 20px; 17 | } 18 | 19 | a { 20 | cursor: pointer; 21 | color: $blue-100; 22 | 23 | &:hover { 24 | color: $blue-150; 25 | } 26 | } 27 | 28 | table { 29 | width: 100%; 30 | border-collapse: collapse; 31 | border-spacing: 0; 32 | } 33 | 34 | td, th { 35 | text-align: left; 36 | padding: 10px 60px 10px 10px; 37 | border-bottom: 1px solid $grey-10; 38 | 39 | &:first-child { 40 | padding-left: 0; 41 | } 42 | 43 | &:last-child { 44 | padding-right: 0; 45 | } 46 | } 47 | 48 | th { 49 | font-weight: $regular; 50 | color: $grey-40; 51 | } 52 | 53 | th.sorted-by { 54 | color: $grey-60; 55 | border-bottom: 1px solid $orange-100; 56 | } 57 | -------------------------------------------------------------------------------- /styles/_layout.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hojberg/mastering-react/dbf48a76bec8a0270569613dcc1d7216f5029770/styles/_layout.scss -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | @import "theme"; 2 | @import "base"; 3 | 4 | // chrome 5 | @import "layout"; 6 | @import "modules/main_header"; 7 | @import "modules/page"; 8 | 9 | // pages 10 | @import "modules/dashboard"; 11 | @import "modules/orders"; 12 | -------------------------------------------------------------------------------- /styles/modules/_dashboard.scss: -------------------------------------------------------------------------------- 1 | .dashboard { 2 | h1 { 3 | margin-bottom: 40px; 4 | } 5 | 6 | .sales-chart { 7 | padding: 20px 40px; 8 | margin: 0 -40px; 9 | background: $grey-10; 10 | 11 | .chart-wrapper { 12 | margin: auto; 13 | } 14 | 15 | .line { 16 | stroke: $orange-100; 17 | stroke-width: 4px; 18 | fill: none; 19 | } 20 | 21 | .axis { 22 | stroke: $grey-40; 23 | stroke-width: 2px; 24 | opacity: 0.5; 25 | } 26 | 27 | .ticks { 28 | text-transform: uppercase; 29 | } 30 | 31 | .y.ticks { 32 | text-anchor: end; 33 | } 34 | 35 | .tick { 36 | font-size: 11px; 37 | fill: $grey-100; 38 | opacity: .2; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /styles/modules/_main_header.scss: -------------------------------------------------------------------------------- 1 | .main-header { 2 | position: relative; 3 | height: 58px; 4 | background: $grey-100; 5 | color: $grey-40; 6 | 7 | .logo { 8 | position: absolute; 9 | top: 21px; 10 | left: 40px; 11 | font-size: 16px; 12 | } 13 | 14 | .main-nav { 15 | position: absolute; 16 | top: 22px; 17 | right: 40px; 18 | font-size: 14px; 19 | 20 | a { 21 | margin-left: 55px; 22 | color: $grey-40; 23 | text-decoration: none; 24 | } 25 | 26 | .active { 27 | color: $orange-80; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /styles/modules/_orders.scss: -------------------------------------------------------------------------------- 1 | .orders { 2 | .page-header { 3 | h1 { 4 | float: left; 5 | } 6 | 7 | .status-nav { 8 | margin-left: 21px; 9 | float: left; 10 | 11 | a { 12 | padding: 0 10px; 13 | color: $grey-40; 14 | font-size: 12px; 15 | line-height: 24px; 16 | vertical-align: -6px; 17 | display: inline-block; 18 | 19 | &:hover { 20 | color: $orange-80; 21 | } 22 | } 23 | 24 | .selected { 25 | color: $orange-100; 26 | } 27 | } 28 | 29 | .amount-filter { 30 | margin-top: 2px; 31 | margin-left: 10px; 32 | padding-left: 20px; 33 | border-left: 1px solid $grey-20; 34 | float: left; 35 | 36 | input { 37 | padding: 4px 6px; 38 | border-radius: 2px; 39 | border: 1px solid $grey-40; 40 | 41 | &:focus { 42 | border: 1px solid $blue-100; 43 | outline: 0; 44 | } 45 | } 46 | } 47 | } 48 | 49 | .orders-table { 50 | .amount { 51 | text-align: right; 52 | } 53 | 54 | .status { 55 | text-align: center; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /styles/modules/_page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | padding: 40px; 3 | 4 | .page-header { 5 | margin-bottom: 60px; 6 | overflow: auto; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /styles/theme.scss: -------------------------------------------------------------------------------- 1 | // font 2 | $font: Roboto, sans-serif; 3 | $light: 300; 4 | $regular: 400; 5 | $medium: 500; 6 | $bold: 700; 7 | 8 | // colors 9 | $white: #fff; 10 | $black: #000; 11 | $grey-100: #42474E; 12 | $grey-60: #898C91; 13 | $grey-40: #BABEC3; 14 | $grey-20: #E1E2E3; 15 | $grey-10: #F5F6F6; 16 | 17 | $orange-100: #FF5722; 18 | $orange-80: #FF7A50; 19 | 20 | $blue-150: #4C829D; 21 | $blue-100: #4FC3F7; 22 | -------------------------------------------------------------------------------- /test/components/orders/order_row_test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js'; 2 | import React from 'react'; 3 | import moment from 'moment'; 4 | import { renderShallow } from '../../test_helper'; 5 | import OrderRow from '../../../src/components/orders/order_row'; 6 | 7 | describe('components/orders/order_row', () => { 8 | let subject; 9 | let order; 10 | 11 | beforeEach(() => { 12 | order = { 13 | reference: 'ASX13X', 14 | customer: 'Bruce Wayne', 15 | orderedAt: moment("2015-03-09T17:14:00.000Z"), 16 | products: [ 17 | { name: 'Blue Wool Shirt', amount: 120.25 }, 18 | { name: 'Pearl White Shirt', amount: 220.50 } 19 | ], 20 | amount: 340.75, 21 | paymentStatus: 'authorized', 22 | orderStatus: 'open' 23 | }; 24 | subject = renderShallow(); 25 | }); 26 | 27 | it('renders the columns', () => { 28 | const tds = subject.props.children.filter((c) => c.type === 'td'); 29 | const tdsContent = tds.map((td) => td.props.children); 30 | expect(tdsContent).to.eql([ 31 | 'ASX13X', 32 | 'Bruce Wayne', 33 | 'Mar 9, 6:14pm', 34 | 'Blue Wool Shirt, Pearl White Shirt', 35 | '$340.75', 36 | 'Authorized', 37 | 'Open' 38 | ]); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/components/orders_test.js: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import sinon from 'sinon'; 3 | import expect from 'expect.js'; 4 | import Orders from '../../src/components/orders'; 5 | import OrdersTable from '../../src/components/orders/orders_table'; 6 | import { setupFakeDOM } from '../test_helper'; 7 | 8 | setupFakeDOM(); 9 | 10 | const TestUtils = React.addons.TestUtils; 11 | 12 | describe('components/orders', () => { 13 | let subject; 14 | 15 | beforeEach(() => { 16 | subject = TestUtils.renderIntoDocument(); 17 | }); 18 | 19 | describe("#render", () => { 20 | describe('when selectedStatus is "all"', () => { 21 | beforeEach(() => { 22 | subject.setState({ selectedStatus: 'all' }); 23 | }); 24 | 25 | it('renders all orders', () => { 26 | const table = TestUtils.findRenderedComponentWithType(subject, OrdersTable); 27 | const statuses = table.props.orders.map((o) => o.orderStatus); 28 | expect( statuses ).to.eql([ 29 | 'open', 'open', 'open', 'shipped' 30 | ]); 31 | }); 32 | }); 33 | 34 | describe('when selectedStatus is "open"', () => { 35 | beforeEach(() => { 36 | subject.setState({ selectedStatus: 'open' }); 37 | }); 38 | 39 | it('renders open orders', () => { 40 | const table = TestUtils.findRenderedComponentWithType(subject, OrdersTable); 41 | const statuses = table.props.orders.map((o) => o.orderStatus); 42 | expect( statuses ).to.eql([ 43 | 'open', 'open', 'open' 44 | ]); 45 | }); 46 | }); 47 | 48 | describe('when selectedStatus is "shipped"', () => { 49 | beforeEach(() => { 50 | subject.setState({ selectedStatus: 'shipped' }); 51 | }); 52 | 53 | it('renders shipped orders', () => { 54 | const table = TestUtils.findRenderedComponentWithType(subject, OrdersTable); 55 | const statuses = table.props.orders.map((o) => o.orderStatus); 56 | expect( statuses ).to.eql([ 57 | 'shipped' 58 | ]); 59 | }); 60 | }); 61 | }); 62 | 63 | describe("#handleStatusClick", () => { 64 | it('calls setState with the new selectedStatus', () => { 65 | sinon.spy( subject, 'setState' ); 66 | subject.handleStatusClick('foo'); 67 | expect( subject.setState.calledWith({ selectedStatus: 'foo' }) ).to.be( true ); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/test_helper.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | import React from 'react/addons'; 3 | const { TestUtils } = React.addons; 4 | 5 | function setupFakeDOM() { 6 | global.document = jsdom.jsdom(""); 7 | global.window = document.parentWindow; 8 | global.navigator = { userAgent: "node.js" }; 9 | require('react/lib/ExecutionEnvironment').canUseDOM = true 10 | } 11 | 12 | function renderShallow(component) { 13 | const renderer = TestUtils.createRenderer(); 14 | renderer.render(component); 15 | return renderer.getRenderOutput(); 16 | } 17 | 18 | export { renderShallow, setupFakeDOM }; 19 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mastering React 6 | 7 | 8 | 9 | 10 |
<%- html %>
11 | 12 | 13 | 14 | --------------------------------------------------------------------------------