Last active
September 27, 2017 21:16
-
-
Save andigu/24b4c11c9b0bcf14d59bd887bf48e4a0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import React, {Component} from "react"; | |
import PropTypes from "prop-types"; | |
import {Card, CardItem, Container, Content, Grid, Header, Icon, Input, Item, Row, Text, View} from "native-base"; | |
import {geocode, search} from "../../lib/Geo"; | |
import {StyleSheet, TouchableOpacity} from "react-native"; | |
import _ from "lodash"; | |
export class AddressSearch extends Component { | |
static propTypes = { | |
onSelect: PropTypes.func, | |
initialValue: PropTypes.string, | |
onBack: PropTypes.func, | |
connected: PropTypes.bool, | |
location: PropTypes.object | |
}; | |
locations: string[] = []; | |
changeText: (text: string) => void; | |
constructor(props) { | |
super(props); | |
this.state = { | |
searchText: props.initialValue, | |
locations: [] | |
}; | |
this.changeText = _.debounce((text) => { | |
search(text, null, this.props.location).then((data) => { | |
this.setState({locations: data}); | |
}); | |
}, 250); | |
this.changeText(props.initialValue); | |
} | |
render() { | |
return <Container> | |
<Header searchBar rounded> | |
<Item> | |
<TouchableOpacity onPress={() => { | |
this.props.onBack(); | |
}}> | |
<Icon name="arrow-back"/> | |
</TouchableOpacity> | |
<Input placeholder="Search" | |
defaultValue={this.props.initialValue} | |
value={this.state.searchText} | |
autoFocus | |
disabled={!this.props.connected} | |
onChangeText={(text) => { | |
this.setState({ | |
searchText: text | |
}); | |
this.changeText(text); | |
}}/> | |
<TouchableOpacity onPress={() => { | |
this.setState({searchText: ""}); | |
this.changeText(""); | |
}}> | |
<Icon name="close"/> | |
</TouchableOpacity> | |
</Item> | |
</Header> | |
<Content keyboardShouldPersistTaps="always"> | |
<Card style={styles.locationList}> | |
{this.props.connected ? | |
<View> | |
<TouchableOpacity onPress={() => { | |
const geo = this.props.location; | |
geocode(geo.coords).then((data: { results: { formatted_address: string }[] }) => { | |
this.props.onBack({ | |
loc: geo.coords, | |
address: data.results[0].formatted_address | |
}); | |
}); | |
}}> | |
<CardItem> | |
<Icon name="locate"/> | |
<Text numberOfLines={1} style={styles.large}> | |
Your location | |
</Text> | |
</CardItem> | |
</TouchableOpacity> | |
{this.state.locations.map((data, i) => | |
<TouchableOpacity onPress={() => { | |
this.props.onBack({ | |
loc: { | |
latitude: data.geometry.location.lat, | |
longitude: data.geometry.location.lng | |
}, | |
address: data.formatted_address | |
}); | |
}} key={i}> | |
<CardItem> | |
<Icon name="pin"/> | |
{data.formatted_address.indexOf(data.name) === -1 ? | |
<Grid> | |
<Row> | |
<Text numberOfLines={1} style={styles.large}>{data.name}</Text> | |
</Row> | |
<Row> | |
<Text numberOfLines={1}> | |
{data.formatted_address} | |
</Text> | |
</Row> | |
</Grid> : | |
<Text numberOfLines={1} style={styles.large}>{data.formatted_address}</Text>} | |
</CardItem> | |
</TouchableOpacity> | |
)}</View> : | |
<CardItem> | |
<Icon name="cloud-outline"/> | |
<Text numberOfLines={1}> | |
You must be connected to wifi to search | |
</Text> | |
</CardItem> | |
} | |
</Card> | |
</Content> | |
</Container>; | |
} | |
} | |
const styles = StyleSheet.create({ | |
locationList: { | |
paddingRight: 40 | |
}, | |
large: {fontSize: 18} | |
}); | |
import React, {Component} from 'react'; | |
import {Body, Button, Content, Form, Icon, Input, Item, Label, Switch, Text} from 'native-base'; | |
import {Field, FormSection, formValueSelector, propTypes, reduxForm} from 'redux-form'; | |
import {LayoutAnimation, Modal, Slider, StyleSheet, TouchableOpacity, View} from "react-native"; | |
import {Map} from "../maps/Map"; | |
import {Metrics, Theme} from "../../theme"; | |
import {connect} from "react-redux"; | |
import type {GeoData, GeoLocation} from "../../lib/Types"; | |
import {geocode} from "../../lib/Geo"; | |
import idx from "idx"; | |
import {AddressSearch} from "./AddressSearch"; | |
import {attachRender, createFields, extractInitialValue, formTypes, setFormValues} from "../../lib/ReduxForm"; | |
import {fieldData as scheduleField, ScheduleForm} from "./ScheduleForm"; | |
import {fieldData as preferenceField, PreferencesForm} from "./Preferences"; | |
import {isDefined, objectMap} from "../../lib/Operators"; | |
import PropTypes from "prop-types"; | |
import autobind from "autobind-decorator"; | |
import Color from "color"; | |
import _ from "lodash"; | |
const fieldData = createFields({ | |
name: {label: "Name", required: true, initialValue: "", type: formTypes.string}, | |
location: {initialValue: {latitude: 43.661331, longitude: -79.398625}, type: formTypes.location}, | |
address: {label: "Address", type: formTypes.customOnFocus}, | |
radius: {label: "Radius", initialValue: 100, type: formTypes.number}, | |
hasSchedule: {label: "Schedule", initialValue: true, type: formTypes.switchType}, | |
schedule: {initialValue: () => objectMap(scheduleField, extractInitialValue)}, | |
preferences: {initialValue: objectMap(preferenceField, val => val.initialValue)} | |
}); | |
export const alarmFormName = 'AlarmForm'; | |
const selector = formValueSelector(alarmFormName); | |
@connect(state => ({value: selector(state, ...Object.values(fieldData).map(field => field.name))}), null) | |
@reduxForm({ | |
form: alarmFormName, | |
initialValues: objectMap(fieldData, (value) => value.initialValue), // redundancy to preserve correct data types | |
validate: values => { | |
const errors = {}; | |
Object.values(fieldData).filter(val => val.required).map(data => data.name).forEach(field => { | |
if (!values[field]) { | |
errors[field] = 'Required'; | |
} | |
}); | |
if (values.schedule && values.schedule.startTime > values.schedule.endTime) { | |
errors.schedule = {}; | |
errors.schedule.startTime = errors.schedule.endTime = 'End time must be after start time'; | |
} | |
return errors; | |
}, | |
keepDirtyOnReinitialize: false, | |
enableReinitialize: true | |
}) | |
export class AlarmForm extends Component { | |
static propTypes = { | |
...propTypes, | |
initialAlarm: PropTypes.object, | |
connected: PropTypes.bool, | |
location: PropTypes.object | |
}; | |
fields = attachRender(fieldData, this.renderInput); | |
state = { | |
searchOpen: false | |
}; | |
componentWillMount() { | |
if (this.props.initialAlarm) { | |
setFormValues(this.props.change, this.props.initialAlarm); | |
} else { | |
const loc: GeoData = this.props.location; | |
this.changeAddress(loc.coords); | |
setFormValues(this.props.change, objectMap(fieldData, extractInitialValue)); | |
this.props.change(this.fields.location.name, {latitude: loc.coords.latitude, longitude: loc.coords.longitude}); | |
} | |
} | |
changeAddress(location: GeoLocation) { | |
geocode(location).then((data) => { | |
this.props.change(this.fields.address.name, idx(data, (x) => x.results[0].formatted_address)); | |
}); | |
} | |
@autobind | |
renderInput({input, label, type, meta: {error, touched}}) { | |
const hasError = isDefined(error); | |
if (label === this.fields.hasSchedule.label) { | |
return <Item itemDivider> | |
<Text>Schedule</Text> | |
<Switch value={input.value} | |
style={styles.marginLeft} | |
onValueChange={(x) => { | |
input.onChange(x); | |
LayoutAnimation.configureNext(LayoutAnimation.Presets.linear); | |
}} | |
thumbTintColor={Theme.brandPrimary} | |
onTintColor={Color(Theme.brandPrimary).lighten(0.8).string()} | |
tintColor="lightgrey"/> | |
</Item>; | |
} | |
switch (type) { | |
case formTypes.location: | |
const name = this.props.value[this.fields.name.name]; | |
return <View style={styles.mapContainer}> | |
<Map locations={[{ | |
onDragEnd: input.onChange, | |
title: isDefined(name) ? name : "", | |
radius: this.props.value[this.fields.radius.name], | |
...input.value | |
}]}/> | |
</View>; | |
case formTypes.number: | |
return <Item style={styles.noInputContainer}> | |
<Label style={styles.sliderLabel}>{label}</Label> | |
<Text style={styles.sliderText}>100m</Text> | |
<Slider style={styles.slider} | |
thumbTintColor={Theme.brandPrimary} | |
maximumTrackTintColor={Theme.brandPrimary} | |
onSlidingComplete={input.onChange} | |
minimumValue={100} | |
maximumValue={1000} | |
value={_.isInteger(input.value) ? input.value : 100}/> | |
<Text style={styles.sliderText}>1000m</Text> | |
</Item>; | |
case formTypes.customOnFocus: | |
return <Item error={hasError} style={styles.inputContainer}> | |
<Label>{label}</Label> | |
<TouchableOpacity onPress={() => { | |
input.onFocus(); | |
}} style={styles.noInputContainer}> | |
{input.value ? <Text numberOfLines={1}>{input.value}</Text> : <View style={styles.inputFiller}/>} | |
</TouchableOpacity> | |
</Item>; | |
default: | |
return <Item error={hasError && touched} style={styles.inputContainer}> | |
<Label>{label}</Label> | |
<Input {...input}/> | |
</Item>; | |
} | |
} | |
render() { | |
const {change, handleSubmit, value} = this.props; | |
return ( | |
<Content keyboardShouldPersistTaps="handled" keyboardDismissMode="none"> | |
<Modal | |
animationType={"slide"} | |
transparent={false} | |
onRequestClose={() => { | |
}} | |
visible={this.state.searchOpen}> | |
<AddressSearch | |
connected={this.props.connected} | |
location={this.props.location} | |
initialValue={this.props.value[this.fields.address.name]} | |
onBack={(data: ?{ loc: GeoLocation, address: string }) => { | |
if (data) { | |
change(this.fields.location.name, data.loc); | |
change(this.fields.address.name, data.address); | |
} | |
this.setState({searchOpen: false}); | |
}}/> | |
</Modal> | |
<Form> | |
<Field {...this.fields.location} | |
onChange={(_, newValue) => { | |
this.changeAddress(newValue); | |
}}/> | |
<Field {...this.fields.name}/> | |
<Field {...this.fields.address} | |
onFocus={() => { | |
this.setState({ | |
searchOpen: true | |
}); | |
}}/> | |
<Field {...this.fields.radius}/> | |
<Field {...this.fields.hasSchedule}/> | |
<FormSection {...this.fields.schedule}> | |
{value.hasSchedule ? <ScheduleForm/> : <View/>} | |
</FormSection> | |
<FormSection {...this.fields.preferences}> | |
<PreferencesForm/> | |
</FormSection> | |
<Body> | |
<Button style={{margin: 10}} primary onPress={handleSubmit} rounded> | |
<Text>Save</Text> | |
<Icon name="checkmark" style={styles.marginLeft} small/> | |
</Button> | |
</Body> | |
</Form> | |
</Content> | |
); | |
} | |
} | |
const styles = StyleSheet.create({ | |
mapContainer: { | |
height: Metrics.screenHeight * 0.5 | |
}, | |
slider: { | |
width: "62%" | |
}, | |
noInputContainer: { | |
paddingVertical: 10, | |
marginRight: 15 | |
}, | |
sliderLabel: { | |
marginRight: 10 | |
}, | |
sliderText: { | |
fontSize: 13 | |
}, | |
inputContainer: { | |
marginHorizontal: 15 | |
}, | |
inputFiller: { | |
width: 200, | |
height: 25 | |
}, | |
marginLeft: { | |
marginLeft: 10 | |
} | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment