Added Header, Icon and MessageModal. Refactor screens into directories.

This commit is contained in:
John Lyon-Smith
2018-04-02 13:22:33 -07:00
parent aa622012cd
commit 410d2fde4f
56 changed files with 556 additions and 461 deletions

View File

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +0,0 @@
import React from 'react';
import App from './App';
import renderer from 'react-test-renderer';
it('renders without crashing', () => {
const rendered = renderer.create(<App />).toJSON();
expect(rendered).toBeTruthy();
});

View File

@@ -1,7 +1,4 @@
{
"expo": {
"sdkVersion": "25.0.0"
},
"name": "DeightonAR",
"displayName": "DeightonAR"
}

View File

@@ -1,4 +1,4 @@
import { AppRegistry } from 'react-native';
import App from './src/App';
import { AppRegistry } from 'react-native'
import App from './src/App'
AppRegistry.registerComponent('DeightonAR', () => App);
AppRegistry.registerComponent('DeightonAR', () => App)

View File

@@ -203,7 +203,6 @@
"${BUILT_PRODUCTS_DIR}/GTMSessionFetcher/GTMSessionFetcher.framework",
"${BUILT_PRODUCTS_DIR}/GoogleToolboxForMac/GoogleToolboxForMac.framework",
"${BUILT_PRODUCTS_DIR}/React/React.framework",
"${BUILT_PRODUCTS_DIR}/ReactNativeNavigation/ReactNativeNavigation.framework",
"${PODS_ROOT}/../../node_modules/react-viro/ios/dist/ViroRenderer/ViroKit.framework",
"${BUILT_PRODUCTS_DIR}/react-native-maps/react_native_maps.framework",
"${BUILT_PRODUCTS_DIR}/yoga/yoga.framework",
@@ -218,7 +217,6 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GTMSessionFetcher.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleToolboxForMac.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/React.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactNativeNavigation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ViroKit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/react_native_maps.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework",

View File

@@ -34,38 +34,15 @@
ReferencedContainer = "container:DeightonAR.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "DeightonARTests.xctest"
BlueprintName = "DeightonARTests"
ReferencedContainer = "container:DeightonAR.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
BuildableName = "DeightonARTests.xctest"
BlueprintName = "DeightonARTests"
ReferencedContainer = "container:DeightonAR.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
@@ -83,6 +60,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

View File

@@ -12,9 +12,6 @@
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
// RNN: react-native-navigation
#import "RCCManager.h"
// RNM: react-native-maps
// See https://developers.google.com/maps/documentation/ios-sdk/start
// and https://gist.github.com/dmachat/412658416dc7dcf3f7a829b7a10c2583
@@ -27,30 +24,25 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
/*
VRTBundleURLProvider *bundleProvider = [[VRTBundleURLProvider alloc] init];
jsCodeLocation = [bundleProvider jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
if (jsCodeLocation == nil) {
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
}
*/
// RNM: Uncomment for Google Maps support
// [GMSServices provideAPIKey:@"AIzaSyDN4E_vzO4cKjKHkMg_49hX1GBnU34kx4U"];
NSURL *jsCodeLocation;
#ifdef DEBUG
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
#endif
// Bootstrap RNN. See https://github.com/wix/react-native-navigation/blob/master/example/ios/example/AppDelegate.m
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"DeightonAR"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.window.backgroundColor = [UIColor whiteColor];
[[RCCManager sharedInstance] initBridgeWithBundleURL:jsCodeLocation launchOptions:launchOptions];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}

View File

@@ -26,8 +26,6 @@ target 'DeightonAR' do
pod 'GLog', :podspec => '../node_modules/react-native/third-party-podspecs/GLog.podspec'
pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'
pod 'ReactNativeNavigation', :podspec => '../node_modules/react-native-navigation/ios/ReactNativeNavigation.podspec'
pod 'react-native-maps', path: '../node_modules/react-native-maps'
# pod 'react-native-google-maps', path: '../node_modules/react-native-maps'
# pod 'GoogleMaps'

View File

@@ -70,8 +70,6 @@ PODS:
- React/Core
- React/fishhook
- React/RCTBlob
- ReactNativeNavigation (1.1.407):
- React
- ViroKit (1.0):
- AWSDynamoDB (~> 2.6.7)
- GVRAudioSDK (= 1.120.0)
@@ -93,7 +91,6 @@ DEPENDENCIES:
- React/RCTNetwork (from `../node_modules/react-native`)
- React/RCTText (from `../node_modules/react-native`)
- React/RCTWebSocket (from `../node_modules/react-native`)
- ReactNativeNavigation (from `../node_modules/react-native-navigation/ios/ReactNativeNavigation.podspec`)
- ViroKit (from `../node_modules/react-viro/ios/dist/ViroRenderer/`)
- ViroReact (from `../node_modules/react-viro/ios/`)
- yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -119,8 +116,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native"
react-native-maps:
:path: "../node_modules/react-native-maps"
ReactNativeNavigation:
:podspec: "../node_modules/react-native-navigation/ios/ReactNativeNavigation.podspec"
ViroKit:
:path: "../node_modules/react-viro/ios/dist/ViroRenderer/"
ViroReact:
@@ -141,11 +136,10 @@ SPEC CHECKSUMS:
GVRSDK: 0cb9d0ce06a84d698e61e3db9509766022dcf815
React: 541ba768b9855e10cdc76f55427a5cd0653ca806
react-native-maps: 066c2afcc89e18726377bcc685315f989ca22449
ReactNativeNavigation: 0a0de59d82ed1accc9b762037752b11d794c7a4c
ViroKit: 9631f301ef6a3f56116b23d6aac5d5c2307aa368
ViroReact: 5520f26ac4654e361786c82da3b29ce0402c3c00
yoga: 17521bbb0dd54a47c0b3ac43253e78cdac7488e0
PODFILE CHECKSUM: b13c8a6ef20af9f80e486813619c4b2833c402f8
PODFILE CHECKSUM: 1de520fb7d323b83502ffda9c5bf993f7545cfa9
COCOAPODS: 1.5.0.beta.1

View File

@@ -5438,6 +5438,19 @@
}
}
},
"react-native-animatable": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.2.4.tgz",
"integrity": "sha512-cVTQXa/cp8gfxcl+l6I1rGAI7EeoNZ0ur9vtxb3tD5iGlJbIyUfQK61e6BycnZewdgQ639Mp6OrueXTpZlv76Q==",
"requires": {
"prop-types": "15.6.1"
}
},
"react-native-iphone-x-helper": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.0.2.tgz",
"integrity": "sha512-5FYNC4kTi/YK86l+r8GQ0xgsSL2tleCQ5Yppu1+ARbnm2qGRmDoJTGSNsWBAWa8FP1ORyhMjxi18IlvSRKaI2g=="
},
"react-native-maps": {
"version": "0.20.1",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-0.20.1.tgz",
@@ -5493,6 +5506,15 @@
}
}
},
"react-native-modal": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-5.4.0.tgz",
"integrity": "sha512-Bvq4FQPMAFijqjqNX6TxLgKOwdbruM6GvFwF9rb+mowbaFZVoYbHTKLaAbdPlrblgaZKWyOuuxBUoDx41+Xktg==",
"requires": {
"prop-types": "15.6.1",
"react-native-animatable": "1.2.4"
}
},
"react-proxy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/react-proxy/-/react-proxy-1.1.8.tgz",

View File

@@ -25,7 +25,9 @@
"react": "^16.2.0",
"react-form-binder": "^1.2.0",
"react-native": "^0.51.1",
"react-native-iphone-x-helper": "^1.0.2",
"react-native-maps": "^0.20.1",
"react-native-modal": "^5.4.0",
"react-router-native": "^4.2.0",
"react-viro": "^2.4.0",
"socket.io-client": "^2.0.4"

View File

@@ -4,6 +4,7 @@ import {
ViroARSceneNavigator, ViroARScene, ViroARPlane, ViroBox, ViroText, ViroAmbientLight
} from 'react-viro'
import autobind from 'autobind-decorator'
import backImage from './images/back.png'
const styles = {
helloWorldTextStyle: {
@@ -31,7 +32,9 @@ class WorkItemSceneAR extends React.Component {
return (
<ViroARScene onTrackingInitialized={()=>{this.setState({text : "Hello World!"})}}>
<ViroAmbientLight color="#ffffff" intensity={200}/>
<ViroText text={this.state.text} scale={[.5, .5, .5]} position={[0, 0, -1]}
<ViroText
text={this.state.text} scale={[.5, .5, .5]}
position={[0, 0, -1]}
style={styles.helloWorldTextStyle} />
<ViroARPlane>
<ViroBox position={[0, .5, 0]} />
@@ -41,11 +44,7 @@ class WorkItemSceneAR extends React.Component {
}
}
export class Viewer extends React.Component {
static navigatorStyle = {
navBarHidden: true,
}
export class ARViewer extends React.Component {
constructor(props) {
super(props)
}
@@ -61,11 +60,11 @@ export class Viewer extends React.Component {
<ViroARSceneNavigator
style={{ width: '100%', height: '100%' }}
apiKey='06F37B6A-74DA-4A83-965A-7DE2209A5C46'
initialScene={{ scene: WorkItemSceneAR }} debug={true} />
initialScene={{ scene: WorkItemSceneAR }} />
<View style={{position: 'absolute', left: 50, right: 0, top: 50}}>
<TouchableHighlight style={styles.buttons} onPress={this._handlePress} underlayColor={'#00000000'} >
<Image source={require("./images/back_arrow.png")} />
<Image source={backImage} />
</TouchableHighlight>
</View>
</View>

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1 @@
export { ARViewer } from './ARViewer'

View File

@@ -1,23 +1,8 @@
import React from 'react'
import { StyleSheet, View, TouchableOpacity, Image, ScrollView } from 'react-native'
import backImage from './images/back.png'
import autobind from 'autobind-decorator'
export class Activity extends React.Component {
static navigatorButtons = {
leftButtons: [
{
id: 'cancel',
title: 'Cancel',
}
],
rightButtons: [
{
id: 'done',
title: 'Done',
}
]
}
static styles = StyleSheet.create({
container: {
height: '100%',
@@ -28,20 +13,10 @@ export class Activity extends React.Component {
constructor(props) {
super(props)
this.props.navigator.setOnNavigatorEvent(this._handleNavigatorEvent.bind(this))
}
_handleNavigatorEvent(event) {
switch (event.id) {
case 'cancel':
case 'done':
this.props.navigator.pop()
break
}
}
_handlePressButton() {
this.props.navigator.pop()
_handlePushButton(event) {
this.props.history.goBack()
}
render() {

View File

@@ -0,0 +1 @@
export { Activity } from './Activity'

View File

@@ -0,0 +1,42 @@
import React, { Fragment, Component } from 'react'
import { api } from '../API'
import { Route, Redirect } from 'react-router-native'
import { View } from 'react-native'
import autobind from 'autobind-decorator'
export class DefaultRoute extends Component {
@autobind
updateComponent() {
this.forceUpdate()
}
componentDidMount() {
api.addListener('login', this.updateComponent)
}
componentWillUnmount() {
api.removeListener('login', this.updateComponent)
}
render() {
const user = api.loggedInUser
let path = null
if (user) {
if (!user.pending) {
path = '/home'
}
} else {
path = '/login'
}
const { location } = this.props
// Render a redirect or nothing until we finished logging on
return (
<Route path='/' render={() => (
path ? <Redirect to={path} /> : null
)} />
)
}
}

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { Platform, StyleSheet, Text, Image, Switch, TextInput,
KeyboardAvoidingView, ScrollView, View, Button, Alert, InteractionManager } from 'react-native';
KeyboardAvoidingView, View, Button } from 'react-native'
import { MessageModal } from '../Modal'
import logoImage from './images/deighton.png'
import { FormBinder } from 'react-form-binder'
import { api } from '../API'
@@ -8,10 +9,6 @@ import { BoundSwitch, BoundInput, BoundButton } from '../ui'
import autobind from 'autobind-decorator'
export class Login extends React.Component {
static navigatorStyle = {
navBarHidden: true,
}
static bindings = {
email: {
alwaysGet: true,
@@ -69,25 +66,37 @@ export class Login extends React.Component {
constructor(props) {
super(props)
this.state = {
binder: new FormBinder({email: 'john@lyon-smith.org'}, Login.bindings)
binder: new FormBinder({email: 'john@lyon-smith.org'}, Login.bindings),
messageModal: null,
}
}
@autobind
handleLogin() {
let obj = this.state.binder.getModifiedFieldValues()
let { navigator } = this.props
let { history } = this.props
if (obj) {
api.login(obj.email, obj.password, obj.rememberMe).then((user) => {
this.props.navigator.dismissAllModals()
history.replace('/home')
}).catch((error) => {
console.error(api.apiURL)
this.setState({ messageModal: {
icon: 'hand',
message: 'Unable to login',
detail: error.message,
}})
})
}
}
@autobind
handleMessageDismiss() {
this.setState({ messageModal: null })
}
render() {
const { messageModal } = this.state
return (
<KeyboardAvoidingView style={Login.styles.page} behavior='padding'
keyboardVerticalOffset={Platform.select({ios: 0, android: -220})}>
@@ -104,6 +113,12 @@ export class Login extends React.Component {
<View style={Login.styles.buttonRow}>
<BoundButton title='Login' name='login' width='100%' onPress={this.handleLogin} binder={this.state.binder} />
</View>
<MessageModal
open={!!messageModal}
icon={messageModal ? messageModal.icon : ''}
message={messageModal ? messageModal.message : ''}
detail={messageModal ? messageModal.detail : ''}
onDismiss={messageModal && this.handleMessageDismiss} />
</KeyboardAvoidingView>
)
}

21
mobile/src/Auth/Logout.js Normal file
View File

@@ -0,0 +1,21 @@
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { api } from '../API'
export class Logout extends Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}
componentDidMount(event) {
const cb = () => {
this.props.history.replace('/login')
}
api.logout().then(cb, cb)
}
render() {
return null
}
}

View File

@@ -0,0 +1,41 @@
import React from 'react'
import { Route, Redirect } from 'react-router-native'
import { PropTypes } from 'prop-types'
import { api } from '../API'
import autobind from 'autobind-decorator'
export class ProtectedRoute extends React.Component {
static propTypes = {
location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }),
admin: PropTypes.bool,
}
@autobind
updateComponent() {
this.forceUpdate()
}
componentDidMount() {
api.addListener('login', this.updateComponent)
}
componentWillUnmount() {
api.removeListener('login', this.updateComponent)
}
render(props) {
const user = api.loggedInUser
if (user) {
if (user.pending) {
// The API might be in the middle of fetching the user information
return null
} else if (!this.props.admin || (this.props.admin && user.administrator)) {
return <Route {...this.props} />
}
}
// TODO: Can add redirect back in here - see website
return <Redirect to='/login' />
}
}

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

4
mobile/src/Auth/index.js Normal file
View File

@@ -0,0 +1,4 @@
export { Login } from './Login'
export { Logout } from './Logout'
export { ProtectedRoute } from './ProtectedRoute'
export { DefaultRoute } from './DefaultRoute'

131
mobile/src/Home/Home.js Normal file
View File

@@ -0,0 +1,131 @@
import React from 'react'
import { StyleSheet, Text, TextInput, FlatList, Image, View, TouchableOpacity } from 'react-native'
import MapView, { Marker } from 'react-native-maps'
import { Icon, Header } from '../ui'
import { api } from '../API'
import autobind from 'autobind-decorator'
import pinImage from './images/pin.png'
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#FFFFFF',
alignItems: 'flex-start',
justifyContent: 'flex-start',
}
})
const data = [
{key: '1', title: 'Remove Animal Carcass', location: 'Ossington Ave. | 0.2 mi.', state: 'planned', latlng: { latitude: 43.653226, longitude: -79.383184 } },
{key: '2', title: 'Fix sign post', location: 'Alexandre St. | 0.7 mi.', state: 'open', latlng: { latitude: 43.648118, longitude: 79.392636 }},
{key: '3', title: 'Overflowing trash', location: 'Bay St. | 0.8 mi.', state: 'open', latlng: { latitude: 43.640168, longitude: -79.409373 }},
{key: '4', title: 'Leaking water pipe', location: 'Bloor St. | 1.2 mi.', state: 'planned', latlng: { latitude: 43.633110, longitude: -79.415880 }},
{key: '5', title: 'Tree branch in road', location: 'Blue Jays Way | 2.2 mi.', state: 'open', latlng: { latitude: 43.653526, longitude: -79.361385 }},
{key: '6', title: 'Washing machine on sidewalk', location: 'Christie St. | 3.0 mi.', state: 'open', latlng: { latitude: 43.663870, longitude: -79.383705 }},
{key: '7', title: 'Dead moose', location: 'Cummer Ave. | 4.2 mi.', state: 'open', latlng: { latitude: 43.659166, longitude: -79.391350 }},
{key: '8', title: 'Glass in street', location: 'Danforth Ave. | 4.7 mi.', state: 'open', latlng: { latitude: 43.663538, longitude: -79.423212 }},
]
export class Home extends React.Component {
constructor(props) {
super(props)
}
@autobind
_handleNavigatorEvent(event) {
switch (event.id) {
case 'logout':
api.logout().then(() => {
this.props.history.replace('/login')
})
break
case 'viewer':
this.props.push('/viewer')
break
}
}
@autobind
handleAdminButton() {
this.props.history.replace('/admin')
}
@autobind
handleItemSelect(item, index) {
this.props.history.replace('/activity')
}
@autobind
handleLogout() {
this.props.history.replace('/logout')
}
render() {
return (
<View style={styles.container}>
<Header title='Work Item Map' leftButton={{ icon: 'logout', onPress: this.handleLogout }} rightButton={{ icon: 'glasses' }} />
<MapView
style={{
width: '100%', height: '50%',
}}
zoomControlEnabled
initialRegion={{
latitude: 43.653908,
longitude: -79.384293,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}>
{
data.map(marker => (
<Marker
key={marker.key}
coordinate={marker.latlng}
title={marker.title}
description={marker.location}
image={pinImage}
anchor={{x: 0.5, y: 1.0}} />
))
}
</MapView>
<View style={{ flexDirection: 'row', alignItems: 'center', width: '100%', height: 40, backgroundColor: '#F4F4F4' }}>
<Icon name='search' size={16} style={{marginLeft: 10, marginRight: 5, tintColor: 'gray' }} />
<TextInput style={{ flexGrow: 1, height: '100%' }} placeholder='Search' />
<Icon name='cancel' size={16} style={{marginLeft: 5, marginRight: 10, tintColor: 'gray' }} />
</View>
<FlatList
style={{ width: '100%', flexGrow: 1 }}
data={data}
renderItem={({item, index}) => {
return (
<View style={{ flexDirection: 'row', height: 50 }}>
<Text style={{ fontSize: 8, width: 45, marginLeft: 5, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
<View style={{ width: '75%', flexDirection: 'column' }}>
<Text style={{ fontSize: 20 }}>{item.title}</Text>
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
</View>
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
<Icon name='rightArrow' size={16} />
</TouchableOpacity>
</View>
)
}} />
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
height: 45,
backgroundColor: '#F4F4F4',
}}>
<TouchableOpacity onPress={this._handleMyLocation}>
<Icon name='center' size={24} style={{ marginLeft: 15, tintColor: 'gray' }} />
</TouchableOpacity>
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text>
<TouchableOpacity onPress={this._handleAdminButton}>
<Icon name='settings' size={24} style={{ marginRight: 15, tintColor: 'gray' }} />
</TouchableOpacity>
</View>
</View>
);
}
}

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

1
mobile/src/Home/index.js Normal file
View File

@@ -0,0 +1 @@
export { Home } from './Home'

View File

@@ -0,0 +1,62 @@
import React, { Component } from 'react'
import Modal from 'react-native-modal'
import PropTypes from 'prop-types'
import { View, Text, TouchableOpacity } from 'react-native'
import { Icon } from '../ui'
import autobind from 'autobind-decorator'
export class MessageModal extends Component {
static propTypes = {
open: PropTypes.bool,
icon: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
detail: PropTypes.string,
onDismiss: PropTypes.func
}
@autobind
handleButtonPress() {
const { onDismiss } = this.props
if (onDismiss) {
onDismiss()
}
}
constructor(props) {
super(props)
this.state = {
}
}
render() {
const { open, icon, message, detail } = this.props
return (
<Modal isVisible={open}>
<View style={{ alignSelf: 'center', padding: 5, backgroundColor: '#FFFFFF', flexDirection: 'row' }}>
<Icon name={icon} size={130} margin={3} />
<View style={{ marginLeft: 20, marginRight: 20, flexGrow: 1, flexDirection: 'column' }}>
<Text style={{ marginTop: 5, fontSize: 18, alignSelf: 'center' }}>{message}</Text>
<Text style={{ marginTop: 20, alignSelf: 'center' }}>{detail}</Text>
<TouchableOpacity onPress={this.handleButtonPress}
style={{
alignSelf: 'center',
backgroundColor: 'blue',
marginTop: 20,
marginBottom: 10,
justifyContent: 'center',
paddingHorizontal: 10,
height: 40,
width: 100,
backgroundColor: '#3BB0FD'
}}>
<Text style={{ alignSelf: 'center', color: 'black' }}>OK</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
)
}
}

View File

@@ -0,0 +1 @@
export { MessageModal } from './MessageModal'

View File

@@ -2,21 +2,6 @@ import React from 'react'
import { StyleSheet, View, TouchableOpacity, Image, ScrollView, Picker, Text } from 'react-native'
export class WorkItem extends React.Component {
static navigatorButtons = {
leftButtons: [
{
id: 'cancel',
title: 'Cancel',
}
],
rightButtons: [
{
id: 'done',
title: 'Done',
}
]
}
static styles = StyleSheet.create({
container: {
height: '100%',
@@ -27,20 +12,6 @@ export class WorkItem extends React.Component {
constructor(props) {
super(props)
this.props.navigator.setOnNavigatorEvent(this._handleNavigatorEvent.bind(this))
}
_handleNavigatorEvent(event) {
switch (event.id) {
case 'cancel':
case 'done':
this.props.navigator.pop()
break
}
}
_handlePressButton() {
this.props.navigator.pop()
}
render() {

View File

@@ -0,0 +1,71 @@
import React from 'react'
import { StyleSheet, View, TouchableOpacity, Image, FlatList, Text} from 'react-native'
import autobind from 'autobind-decorator'
const styles = StyleSheet.create({
container: {
height: '100%',
width: '100%',
justifyContent: 'flex-start',
backgroundColor: '#FFFFFF',
},
})
const data = [
{key: '1', type: 'work', location: 'Ossington Ave. | 0.2 mi.', state: 'open', latlng: { latitude: 43.653226, longitude: -79.383184 } },
{key: '2', type: 'inspection', location: 'Alexandre St. | 0.7 mi.', state: 'open', latlng: { latitude: 43.648118, longitude: 79.392636 }},
{key: '3', type: 'complaint', location: 'Bay St. | 0.8 mi.', state: 'open', latlng: { latitude: 43.640168, longitude: -79.409373 }},
{key: '4', type: 'work', location: 'Bloor St. | 1.2 mi.', state: 'open', latlng: { latitude: 43.633110, longitude: -79.415880 }},
{key: '5', type: 'inspection', location: 'Blue Jays Way | 2.2 mi.', state: 'open', latlng: { latitude: 43.653526, longitude: -79.361385 }},
{key: '6', type: 'complaint', location: 'Christie St. | 3.0 mi.', state: 'open', latlng: { latitude: 43.663870, longitude: -79.383705 }},
{key: '7', type: 'work', location: 'Cummer Ave. | 4.2 mi.', state: 'open', latlng: { latitude: 43.659166, longitude: -79.391350 }},
{key: '8', type: 'complaint', location: 'Danforth Ave. | 4.7 mi.', state: 'open', latlng: { latitude: 43.663538, longitude: -79.423212 }},
]
const inspectionTypes = {
work: {
title: 'Work Order'
},
inspection: {
title: 'Inspection',
},
complaint: {
title: 'Complaint',
}
}
export class WorkItemList extends React.Component {
constructor(props) {
super(props)
this._handleItemSelect = this._handleItemSelect.bind(this)
}
@autobind
_handleItemSelect(item, index) {
this.props.history.push('/activity')
}
render() {
return (
<View style={styles.container}>
<FlatList
style={{ width: '100%', flexGrow: 1, paddingTop: 20, paddingBottom: 20 }}
data={data}
renderItem={({item, index}) => {
return (
<View style={{ flexDirection: 'row', height: 50 }}>
<Text style={{ fontSize: 8, width: 45, marginLeft: 15, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
<View style={{ flexDirection: 'column', width: '75%' }}>
<Text style={{ fontSize: 20 }}>{Admin.inspectionTypes[item.type].title}</Text>
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
</View>
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
<Image source={rightArrowImage} style={{ width: 16, height: 16 }} />
</TouchableOpacity>
</View>
)
}} />
</View>
);
}
}

View File

@@ -0,0 +1,3 @@
export { WorkItem } from './WorkItem'
export { WorkItemList } from './WorkItemList'

View File

@@ -1,11 +1,15 @@
import React from 'react'
import { View, StyleSheet, Text, TouchableHighlight, Image } from 'react-native'
import { View, StyleSheet } from 'react-native'
import {
ViroARSceneNavigator, ViroARScene, ViroARPlane, ViroBox
} from 'react-viro'
import { NativeRouter, Route, Link } from 'react-router-native'
import { NativeRouter, Route, Link, Switch } from 'react-router-native'
import MapView from 'react-native-maps'
import { Home, Viewer, WorkItem, Admin, Login, Activity } from './screens'
import { WorkItem, WorkItemList } from './WorkItem'
import { Activity } from './Activity'
import { Home } from './Home'
import { ARViewer } from './ARViewer'
import { Login, Logout, ProtectedRoute, DefaultRoute } from './Auth'
// See https://github.com/facebook/react-native/issues/12981
console.ignoredYellowBox = [
@@ -16,13 +20,17 @@ export default class App extends React.Component {
render() {
return (
<NativeRouter>
<View style={styles.container}>
<Route exact path="/" component={Home}/>
<Route path="/viewer" component={Viewer}/>
<Route path="/workitem" component={WorkItem}/>
<Route path="/admin" component={Admin}/>
<Route path="/login" component={Login}/>
<Route path="/activity" component={Activity}/>
<View style={{ width: '100%', height: '100%' }}>
<Switch>
<Route exact path='/login' component={Login}/>
<Route exact path='/logout' component={Logout}/>
<ProtectedRoute exact path='/home' component={Home}/>
<ProtectedRoute exact path='/viewer' component={ARViewer}/>
<ProtectedRoute exact path='/activity' component={Activity}/>
<ProtectedRoute exact admin path='/workitem' component={WorkItem}/>
<ProtectedRoute exact admin path='/workitemlist' component={WorkItemList}/>
<DefaultRoute />
</Switch>
</View>
</NativeRouter>
)

View File

@@ -1,93 +0,0 @@
import React from 'react'
import { StyleSheet, View, TouchableOpacity, Image, FlatList, Text} from 'react-native'
import backImage from './images/back.png'
import rightArrowImage from './images/right-arrow.png'
export class Admin extends React.Component {
static navigatorButtons = {
leftButtons: [
{
id: 'back',
icon: require('./images/back.png'),
}
]
}
static data = [
{key: '1', type: 'work', location: 'Ossington Ave. | 0.2 mi.', state: 'open', latlng: { latitude: 43.653226, longitude: -79.383184 } },
{key: '2', type: 'inspection', location: 'Alexandre St. | 0.7 mi.', state: 'open', latlng: { latitude: 43.648118, longitude: 79.392636 }},
{key: '3', type: 'complaint', location: 'Bay St. | 0.8 mi.', state: 'open', latlng: { latitude: 43.640168, longitude: -79.409373 }},
{key: '4', type: 'work', location: 'Bloor St. | 1.2 mi.', state: 'open', latlng: { latitude: 43.633110, longitude: -79.415880 }},
{key: '5', type: 'inspection', location: 'Blue Jays Way | 2.2 mi.', state: 'open', latlng: { latitude: 43.653526, longitude: -79.361385 }},
{key: '6', type: 'complaint', location: 'Christie St. | 3.0 mi.', state: 'open', latlng: { latitude: 43.663870, longitude: -79.383705 }},
{key: '7', type: 'work', location: 'Cummer Ave. | 4.2 mi.', state: 'open', latlng: { latitude: 43.659166, longitude: -79.391350 }},
{key: '8', type: 'complaint', location: 'Danforth Ave. | 4.7 mi.', state: 'open', latlng: { latitude: 43.663538, longitude: -79.423212 }},
]
static inspectionTypes = {
work: {
title: 'Work Order'
},
inspection: {
title: 'Inspection',
},
complaint: {
title: 'Complaint',
}
}
static styles = StyleSheet.create({
container: {
height: '100%',
width: '100%',
justifyContent: 'flex-start',
backgroundColor: '#FFFFFF',
},
})
constructor(props) {
super(props)
this.props.navigator.setOnNavigatorEvent(this._handleNavigatorEvent.bind(this))
this._handleItemSelect = this._handleItemSelect.bind(this)
}
_handleNavigatorEvent(event) {
switch (event.id) {
case 'back':
this.props.navigator.pop()
break
}
}
_handleItemSelect(item, index) {
this.props.navigator.push({
screen: 'app.WorkItem',
title: 'Work Item',
passProps: { item, index },
})
}
render() {
return (
<View style={Admin.styles.container}>
<FlatList
style={{ width: '100%', flexGrow: 1, paddingTop: 20, paddingBottom: 20 }}
data={Admin.data}
renderItem={({item, index}) => {
return (
<View style={{ flexDirection: 'row', height: 50 }}>
<Text style={{ fontSize: 8, width: 45, marginLeft: 15, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
<View style={{ flexDirection: 'column', width: '75%' }}>
<Text style={{ fontSize: 20 }}>{Admin.inspectionTypes[item.type].title}</Text>
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
</View>
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
<Image source={rightArrowImage} style={{ width: 16, height: 16 }} />
</TouchableOpacity>
</View>
)
}} />
</View>
);
}
}

View File

@@ -1,25 +0,0 @@
import React from 'react';
import { StyleSheet, Text, Image, Button } from 'react-native';
export class Error extends React.Component {
static navigatorStyle = {
navBarHidden: true,
}
static styles = StyleSheet.create({
page: {
minHeight: '50%',
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
})
render() {
return (
<View style={Login.styles.page}>
</View>
)
}
}

View File

@@ -1,166 +0,0 @@
import React from 'react'
import { StyleSheet, Text, TextInput, FlatList, Image, View, TouchableOpacity } from 'react-native'
import MapView, { Marker } from 'react-native-maps'
import { api } from '../API'
import centerImage from './images/center.png'
import settingsImage from './images/settings.png'
import searchImage from './images/search.png'
import cancelImage from './images/cancel.png'
import pinImage from './images/pin.png'
import rightArrowImage from './images/right-arrow.png'
export class Home extends React.Component {
static navigatorButtons = {
rightButtons: [
{
id: 'viewer',
icon: require('./images/ar-glases.png'),
}
],
leftButtons: [
{
id: 'logout',
icon: require('./images/logout.png'),
}
]
}
static styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'flex-start',
justifyContent: 'flex-start',
}
})
static data = [
{key: '1', title: 'Remove Animal Carcass', location: 'Ossington Ave. | 0.2 mi.', state: 'planned', latlng: { latitude: 43.653226, longitude: -79.383184 } },
{key: '2', title: 'Fix sign post', location: 'Alexandre St. | 0.7 mi.', state: 'open', latlng: { latitude: 43.648118, longitude: 79.392636 }},
{key: '3', title: 'Overflowing trash', location: 'Bay St. | 0.8 mi.', state: 'open', latlng: { latitude: 43.640168, longitude: -79.409373 }},
{key: '4', title: 'Leaking water pipe', location: 'Bloor St. | 1.2 mi.', state: 'planned', latlng: { latitude: 43.633110, longitude: -79.415880 }},
{key: '5', title: 'Tree branch in road', location: 'Blue Jays Way | 2.2 mi.', state: 'open', latlng: { latitude: 43.653526, longitude: -79.361385 }},
{key: '6', title: 'Washing machine on sidewalk', location: 'Christie St. | 3.0 mi.', state: 'open', latlng: { latitude: 43.663870, longitude: -79.383705 }},
{key: '7', title: 'Dead moose', location: 'Cummer Ave. | 4.2 mi.', state: 'open', latlng: { latitude: 43.659166, longitude: -79.391350 }},
{key: '8', title: 'Glass in street', location: 'Danforth Ave. | 4.7 mi.', state: 'open', latlng: { latitude: 43.663538, longitude: -79.423212 }},
]
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this._handleNavigatorEvent.bind(this))
this._handleAdminButton = this._handleAdminButton.bind(this)
this._handleItemSelect = this._handleItemSelect.bind(this)
}
_handleNavigatorEvent(event) {
switch (event.id) {
case 'logout':
api.logout().then(() => {
this.props.navigator.showModal({ screen: 'app.Login' })
})
break
case 'viewer':
this.props.navigator.push({
screen: 'app.Viewer',
animation: 'slide-horizontal',
})
break
case 'willAppear':
break
case 'didAppear':
if (!api.loggedInUser) {
this.props.navigator.showModal({ screen: 'app.Login' })
}
break
case 'willDisappear':
break
case 'didDisappear':
break
case 'willCommitPreview':
break
}
}
_handleAdminButton() {
this.props.navigator.push({
screen: 'app.Admin',
title: 'Work Items',
})
}
_handleItemSelect(item, index) {
this.props.navigator.push({
screen: 'app.Activity',
title: 'Activity',
passProps: { item, index },
})
}
render() {
return (
<View style={Home.styles.container}>
<MapView
style={{
width: '100%', height: '50%',
}}
zoomControlEnabled
initialRegion={{
latitude: 43.653908,
longitude: -79.384293,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}>
{
Home.data.map(marker => (
<Marker
key={marker.key}
coordinate={marker.latlng}
title={marker.title}
description={marker.location}
image={pinImage}
anchor={{x: 0.5, y: 1.0}} />
))
}
</MapView>
<View style={{ flexDirection: 'row', alignItems: 'center', width: '100%', height: 40, backgroundColor: '#F4F4F4' }}>
<Image source={searchImage} style={{ width: 16, height: 16, tintColor: 'gray', marginLeft: 10, marginRight: 5 }} />
<TextInput style={{ flexGrow: 1, height: '100%' }} placeholder='Search' />
<Image source={cancelImage} style={{ width: 16, height: 16, marginLeft: 5, marginRight: 10 }} />
</View>
<FlatList
style={{ width: '100%', flexGrow: 1 }}
data={Home.data}
renderItem={({item, index}) => {
return (
<View style={{ flexDirection: 'row', height: 50 }}>
<Text style={{ fontSize: 8, width: 45, marginLeft: 5, alignSelf: 'center' }}>{item.state.toUpperCase()}</Text>
<View style={{ width: '75%', flexDirection: 'column' }}>
<Text style={{ fontSize: 20 }}>{item.title}</Text>
<Text style={{ fontSize: 14, color: 'gray' }}>{item.location}</Text>
</View>
<TouchableOpacity style={{ alignSelf: 'center' }} onPress={() => (this._handleItemSelect(item, index))} >
<Image source={rightArrowImage} style={{ width: 16, height: 16 }} />
</TouchableOpacity>
</View>
)
}} />
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
height: 45,
backgroundColor: '#F4F4F4',
}}>
<TouchableOpacity onPress={this._handleMyLocation}>
<Image source={centerImage} style={{ height: 24, width: 24, marginLeft: 15, tintColor: 'gray' }} />
</TouchableOpacity>
<Text style={{ color: 'gray', fontSize: 20, }}>Hide List</Text>
<TouchableOpacity onPress={this._handleAdminButton}>
<Image source={settingsImage} style={{ height: 24, width: 24, marginRight: 15, tintColor: 'gray' }} />
</TouchableOpacity>
</View>
</View>
);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,18 +0,0 @@
import { Home } from './Home'
import { Login } from './Login'
import { Error } from './Error'
import { Viewer } from './Viewer'
import { Activity } from './Activity'
import { WorkItem } from './WorkItem'
import { Admin } from './Admin'
import { Navigation } from 'react-native-navigation'
export function registerScreens() {
Navigation.registerComponent('app.Home', () => Home)
Navigation.registerComponent('app.Login', () => Login)
Navigation.registerComponent('app.Viewer', () => Viewer)
Navigation.registerComponent('app.Activity', () => Activity)
Navigation.registerComponent('app.Admin', () => Admin)
Navigation.registerComponent('app.WorkItem', () => WorkItem)
Navigation.registerComponent('app.Error', () => Error)
}

39
mobile/src/ui/Header.js Normal file
View File

@@ -0,0 +1,39 @@
import React, { Component } from 'react'
import { View, TouchableOpacity, Text } from 'react-native'
import { Icon } from './Icon'
import PropTypes from 'prop-types'
import { ifIphoneX } from 'react-native-iphone-x-helper'
const headerButtonShape = { icon: PropTypes.string.isRequired, onPress: PropTypes.func }
export class Header extends Component {
static propTypes = {
title: PropTypes.string.isRequired,
leftButton: PropTypes.shape(headerButtonShape),
rightButton: PropTypes.shape(headerButtonShape),
}
render() {
const { title, leftButton, rightButton } = this.props
return (
<View style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
width: '100%',
height: 45,
backgroundColor: '#F4F4F4',
...ifIphoneX({ marginTop: 50 }, { marginTop: 20 })
}}>
<TouchableOpacity onPress={leftButton.onPress}>
<Icon name={leftButton.icon} size={24} style={{ marginLeft: 15, tintColor: 'gray' }} />
</TouchableOpacity>
<Text style={{ color: 'gray', fontSize: 18, }}>{title}</Text>
<TouchableOpacity onPress={rightButton.onPress}>
<Icon name={rightButton.icon} size={24} style={{ marginRight: 15, tintColor: 'gray' }} />
</TouchableOpacity>
</View>
)
}
}

37
mobile/src/ui/Icon.js Normal file
View File

@@ -0,0 +1,37 @@
import React, { Component } from 'react'
import { Image } from 'react-native'
import PropTypes from 'prop-types'
const images = {
logout: require('./images/logout.png'),
glasses: require('./images/ar-glasses.png'),
back: require('./images/back.png'),
hand: require('./images/hand.png'),
center: require('./images/center.png'),
rightArrow: require('./images/right-arrow.png'),
search: require('./images/search.png'),
settings: require('./images/settings.png'),
}
export class Icon extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.number,
margin: PropTypes.number,
style: PropTypes.object,
}
static defaultProps = {
size: 50,
margin: 0,
}
render() {
let { size, name, margin, style } = this.props
let source = images[name] || images['hand']
size -= margin * 2
return <Image style={[{ width: size, height: size, margin }, style]} source={source} resizeMode='stretch' />
}
}

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,3 +1,5 @@
export { BoundSwitch } from './BoundSwitch'
export { BoundInput } from './BoundInput'
export { BoundButton } from './BoundButton'
export { Icon } from './Icon'
export { Header } from './Header'

View File

@@ -77,7 +77,7 @@ export class AuthRoutes {
}
let cr = credential()
const isValid = cr.verify(JSON.stringify(user.passwordHash), req.body.password)
const isValid = await cr.verify(JSON.stringify(user.passwordHash), req.body.password)
if (isValid) {
user.loginToken = loginToken.pack(user._id.toString(), user.email)

View File

@@ -20,14 +20,14 @@ export class DefaultRoute extends Component {
render() {
const user = api.loggedInUser
let redirect = null
let path = null
if (user) {
if (!user.pending) {
redirect = <Redirect to={user.administrator ? '/home' : '/profile'} />
path = user.administrator ? '/home' : '/profile'
}
} else {
redirect = <Redirect to='/login' />
path = '/login'
}
return (
@@ -37,7 +37,7 @@ export class DefaultRoute extends Component {
return (
<Fragment>
<Column.Item grow />
{redirect}
{path ? <Redirect to={path} /> : null}
</Fragment>
)
}}

View File

@@ -1,5 +1,5 @@
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { Route, Redirect } from 'react-router'
import { PropTypes } from 'prop-types'
import { api } from 'src/API'
import autobind from 'autobind-decorator'