From 4507e122e1443b4ca1dbf3f589cc4a8a03589427 Mon Sep 17 00:00:00 2001 From: Ananta Pandey Date: Thu, 13 Sep 2018 17:57:26 -0400 Subject: [PATCH] Add add a note flow (#47) --- expo_project/components/MapWithMarkers.js | 17 +- expo_project/components/NoteModal.js | 90 +++++++++ expo_project/components/PersonIcon.js | 6 +- expo_project/components/Selectable.js | 2 +- expo_project/components/Survey.js | 25 ++- expo_project/components/SurveyHeader.js | 2 +- expo_project/config/studies.js | 12 +- expo_project/screens/StudyIndexScreen.js | 5 +- expo_project/screens/SurveyScreen.js | 230 +++++++++++++++------- 9 files changed, 282 insertions(+), 107 deletions(-) create mode 100644 expo_project/components/NoteModal.js diff --git a/expo_project/components/MapWithMarkers.js b/expo_project/components/MapWithMarkers.js index c3f449c..8505f0f 100644 --- a/expo_project/components/MapWithMarkers.js +++ b/expo_project/components/MapWithMarkers.js @@ -6,6 +6,7 @@ import { AnimatedCircularProgress } from 'react-native-circular-progress'; import { iconColors } from '../constants/Colors'; import MapConfig from '../constants/Map'; import PersonIcon from '../components/PersonIcon'; +import * as _ from 'lodash'; // NOTE: A longPress is more like 500ms, // however there's a delay between when the longPress is registered @@ -22,21 +23,22 @@ class MapWithMarkers extends React.Component { this.state = { region: MapConfig.defaultRegion, circularProgressLocation: null, - nextMarkerColor: this.getRandomIconColor(), + nextMarkerColor: null, }; } getRandomIconColor = () => { - const iconOptions = Object.values(iconColors); - return iconOptions[Math.floor(Math.random() * iconOptions.length)]; - }; - - setNextColor = () => { - this.setState({ nextMarkerColor: this.getRandomIconColor() }); + // enforce next color is not current color + const iconColorOptions = _.filter( + _.values(iconColors), + color => color !== this.state.nextMarkerColor, + ); + return _.sample(iconColorOptions); }; startProgressAnimation = (locationX, locationY) => { this.setState({ + nextMarkerColor: this.getRandomIconColor(), circularProgressLocation: { top: locationY - CIRCULAR_PROGRESS_SIZE / 2, left: locationX - CIRCULAR_PROGRESS_SIZE / 2, @@ -47,7 +49,6 @@ class MapWithMarkers extends React.Component { stopProgressAnimation = () => { this.setState({ circularProgressLocation: null, - nextMarkerColor: this.getRandomIconColor(), }); }; diff --git a/expo_project/components/NoteModal.js b/expo_project/components/NoteModal.js new file mode 100644 index 0000000..f538d14 --- /dev/null +++ b/expo_project/components/NoteModal.js @@ -0,0 +1,90 @@ +import React from 'react'; +import { Modal, StyleSheet, Text, View } from 'react-native'; +import { Button, TextInput } from 'react-native-paper'; +import PropTypes from 'prop-types'; + +class NoteModal extends React.Component { + constructor(props) { + super(props); + + this.state = { + text: props.initialValue, + }; + } + + render() { + return ( + + + + + + + + this.setState({ text })} + onSubmitEditing={() => { + this.props.onClose(this.state.text); + }} + /> + + + + + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + padding: 20, + backgroundColor: 'white', + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + modalBody: { + padding: 20, + }, + buttonWrapper: { + marginTop: 20, + }, +}); + +NoteModal.propTypes = { + initialValue: PropTypes.string, + onClose: PropTypes.func, +}; + +NoteModal.defaultProps = { + initialValue: '', + onClose: () => null, +}; + +export default NoteModal; diff --git a/expo_project/components/PersonIcon.js b/expo_project/components/PersonIcon.js index 09f62ab..d45f778 100644 --- a/expo_project/components/PersonIcon.js +++ b/expo_project/components/PersonIcon.js @@ -48,8 +48,12 @@ const styles = StyleSheet.create({ PersonIcon.propTypes = { size: PropTypes.number.isRequired, - backgroundColor: PropTypes.string.isRequired, + backgroundColor: PropTypes.string, shadow: PropTypes.bool, }; +PropTypes.defaultProps = { + backgroundColor: 'white', +}; + export default PersonIcon; diff --git a/expo_project/components/Selectable.js b/expo_project/components/Selectable.js index c429f24..28e6fc6 100644 --- a/expo_project/components/Selectable.js +++ b/expo_project/components/Selectable.js @@ -64,7 +64,7 @@ const styles = StyleSheet.create({ borderWidth: 1, backgroundColor: '#FAFAFA', borderRadius: 3, - borderColor: 'rgba(0, 0, 0, 0.0980392)', + borderColor: 'rgba(0, 0, 0, 0.12)', padding: 5, marginRight: 5, marginTop: 10, diff --git a/expo_project/components/Survey.js b/expo_project/components/Survey.js index 761a7ec..e572a81 100644 --- a/expo_project/components/Survey.js +++ b/expo_project/components/Survey.js @@ -11,10 +11,6 @@ class Survey extends React.Component { const { activeMarker, onSelect } = this.props; return ( - - {activeMarker.title} - {activeMarker.dateLabel} - {_.map(QUESTION_CONFIG, question => { const { questionKey, questionLabel, options } = question; return ( @@ -30,14 +26,31 @@ class Survey extends React.Component { /> ); })} + {activeMarker.note && ( + + Note + + {activeMarker.note} + + + )} ); } } const styles = StyleSheet.create({ - titleContainer: { paddingVertical: 10, paddingHorizontal: 20 }, - title: { fontWeight: 'bold' }, + noteContainer: { paddingVertical: 10 }, + noteTitle: { + // match selectable style + marginBottom: 5, + paddingHorizontal: 20, + }, + noteBody: { + fontFamily: 'monaco', + marginVertical: 10, + marginHorizontal: 20, + }, }); Survey.propTypes = { diff --git a/expo_project/components/SurveyHeader.js b/expo_project/components/SurveyHeader.js index 29164d4..57bc918 100644 --- a/expo_project/components/SurveyHeader.js +++ b/expo_project/components/SurveyHeader.js @@ -31,7 +31,7 @@ const styles = StyleSheet.create({ text: { color: '#fff', fontWeight: '600', - fontSize: 24, + fontSize: 18, fontFamily: Theme.fonts.medium, }, }); diff --git a/expo_project/config/studies.js b/expo_project/config/studies.js index 823b0a6..c7737b4 100644 --- a/expo_project/config/studies.js +++ b/expo_project/config/studies.js @@ -8,19 +8,9 @@ export default [ surveys: [ { type: 'activity', - title: 'Stationary Mapping tool', + title: '2pm - 3pm shift', routeName: 'SurveyScreen', }, - { - type: 'lineOfSight', - title: 'Line of Sight tool', - routeName: 'ComingSoonScreen', - }, - { - type: 'intercept', - title: 'Intercept study tool', - routeName: 'ComingSoonScreen', - }, ], }, ]; diff --git a/expo_project/screens/StudyIndexScreen.js b/expo_project/screens/StudyIndexScreen.js index 7656f23..2f142cf 100644 --- a/expo_project/screens/StudyIndexScreen.js +++ b/expo_project/screens/StudyIndexScreen.js @@ -21,14 +21,14 @@ class SurveyIndexScreen extends React.Component { Your studies {studies.map(study => ( - + {study.studyName} by {study.studyAuthor} {study.surveys.map(survey => { return ( - + @@ -78,7 +78,6 @@ const styles = StyleSheet.create({ }, sectionTitle: { backgroundColor: 'white', - fontWeight: 'bold', marginBottom: 10, }, surveyRow: { diff --git a/expo_project/screens/SurveyScreen.js b/expo_project/screens/SurveyScreen.js index f4183fd..9ad3185 100644 --- a/expo_project/screens/SurveyScreen.js +++ b/expo_project/screens/SurveyScreen.js @@ -6,21 +6,22 @@ import { Platform, StyleSheet, ScrollView, - View, + Text, TouchableOpacity, + View, } from 'react-native'; -import { Button, Paragraph } from 'react-native-paper'; +import { Button, Paragraph, Divider } from 'react-native-paper'; import { withNavigation } from 'react-navigation'; import * as _ from 'lodash'; import moment from 'moment'; import MapWithMarkers from '../components/MapWithMarkers'; -import MarkerCarousel from '../components/MarkerCarousel'; +import PersonIcon from '../components/PersonIcon'; import Survey from '../components/Survey'; -import { iconColors } from '../constants/Colors'; import Layout from '../constants/Layout'; import firebase from '../lib/firebaseSingleton'; -import SurveyHeader from '../components/SurveyHeader'; +import Theme from '../constants/Theme'; +import NoteModal from '../components/NoteModal'; // TODO (Ananta): shouold be dynamically set const MIN_DRAWER_TRANSLATE_Y = 0; @@ -28,25 +29,9 @@ const MID_DRAWER_TRANSLATE_Y = Layout.drawer.height - 300; const MAX_DRAWER_TRANSLATE_Y = Layout.drawer.height - 100; // mostly collapsed, with just the header peaking out const INITIAL_DRAWER_TRANSLATE_Y = MAX_DRAWER_TRANSLATE_Y; -function _markerToDataPoint(marker) { - const dataPoint = {}; - fields = ['gender', 'groupSize', 'mode', 'object', 'posture', 'timestamp', 'location']; - fields.forEach(field => { - if (marker[field]) { - dataPoint[field] = marker[field]; - } - }); - - return dataPoint; -} - class Indicator extends React.Component { render() { - return ( - - - - ); + return ; } } @@ -66,9 +51,25 @@ class Instructions extends React.Component { } class SurveyScreen extends React.Component { - static navigationOptions = { - headerTitle: , - }; + static navigationOptions = ({ navigation }) => ({ + headerTitle: navigation.getParam('surveyTitle'), + headerLeft: ( + + ), + }); constructor(props) { super(props); @@ -80,6 +81,7 @@ class SurveyScreen extends React.Component { this.state = { activeMarkerId: null, markers: [], + modalVisible: false, formScrollPosition: 0, pan: new Animated.ValueXY({ x: 0, y: INITIAL_DRAWER_TRANSLATE_Y }), }; @@ -87,6 +89,7 @@ class SurveyScreen extends React.Component { this.state.pan.addListener(value => (this._drawerY = value.y)); this.resetDrawer = this.resetDrawer.bind(this); + this.getToggleDirection = this.getToggleDirection.bind(this); this.toggleDrawer = this.toggleDrawer.bind(this); this.selectMarker = this.selectMarker.bind(this); this.createNewMarker = this.createNewMarker.bind(this); @@ -113,11 +116,12 @@ class SurveyScreen extends React.Component { const marker = { id: doc.id, ...doc.data(), - color: _.sample(_.values(iconColors)), }; markers.push(marker); }); - this.setState({ markers }); + if (markers.length) { + this.setState({ markers, activeMarkerId: markers[0].id }); + } }); } @@ -171,8 +175,8 @@ class SurveyScreen extends React.Component { y, }, useNativeDriver: true, - friction: 5, - }).start(); + friction: 6, + }).start(() => this.setState(this.state)); if (this.state.formScrollPosition) { this.scrollView.scrollTo({ @@ -184,14 +188,22 @@ class SurveyScreen extends React.Component { }); } + getToggleDirection() { + const direction = this._drawerY === MIN_DRAWER_TRANSLATE_Y ? 'down' : 'up'; + return direction; + } + toggleDrawer() { const y = - this._drawerY === MIN_DRAWER_TRANSLATE_Y ? MAX_DRAWER_TRANSLATE_Y : MIN_DRAWER_TRANSLATE_Y; + this.getToggleDirection() === 'down' ? MAX_DRAWER_TRANSLATE_Y : MIN_DRAWER_TRANSLATE_Y; Animated.timing(this.state.pan, { toValue: { x: 0, y }, duration: 200, useNativeDriver: true, - }).start(); + }).start(() => { + // hack to trigger a re-render + this.setState(this.state); + }); if (this.state.formScrollPosition) { this.scrollView.scrollTo({ y: 0, animated: false }); @@ -211,7 +223,7 @@ class SurveyScreen extends React.Component { } } - setFormResponse(id, key, value, selectableHeight) { + setFormResponse(id, key, value, heightToScroll) { const markersCopy = [...this.state.markers]; const marker = _.find(markersCopy, { id, @@ -231,34 +243,36 @@ class SurveyScreen extends React.Component { .doc(surveyId) .collection('dataPoints') .doc(marker.id) - .set(_markerToDataPoint(marker)); - - const currentScrollPosition = this.state.formScrollPosition; - const currentDrawerOffset = this._drawerY; - const newDrawerOffset = currentDrawerOffset - selectableHeight; - - if (newDrawerOffset >= MIN_DRAWER_TRANSLATE_Y) { - Animated.timing(this.state.pan, { - toValue: { x: 0, y: newDrawerOffset }, - duration: 200, - useNativeDriver: true, - }).start(); - } else if (currentDrawerOffset > MIN_DRAWER_TRANSLATE_Y) { - // Animate drawer to the top - // then scroll the remaining amount to ensure next question is visible - const remainder = currentDrawerOffset - MIN_DRAWER_TRANSLATE_Y; - Animated.timing(this.state.pan, { - toValue: { x: 0, y: MIN_DRAWER_TRANSLATE_Y }, - duration: 200, - useNativeDriver: true, - }).start(); - this.scrollView.scrollTo({ - y: currentScrollPosition + selectableHeight - remainder, - }); - } else { - this.scrollView.scrollTo({ - y: currentScrollPosition + selectableHeight, - }); + .set(marker); + + if (heightToScroll) { + const currentScrollPosition = this.state.formScrollPosition; + const currentDrawerOffset = this._drawerY; + const newDrawerOffset = currentDrawerOffset - heightToScroll; + + if (newDrawerOffset >= MIN_DRAWER_TRANSLATE_Y) { + Animated.timing(this.state.pan, { + toValue: { x: 0, y: newDrawerOffset }, + duration: 200, + useNativeDriver: true, + }).start(); + } else if (currentDrawerOffset > MIN_DRAWER_TRANSLATE_Y) { + // Animate drawer to the top + // then scroll the remaining amount to ensure next question is visible + const remainder = currentDrawerOffset - MIN_DRAWER_TRANSLATE_Y; + Animated.timing(this.state.pan, { + toValue: { x: 0, y: MIN_DRAWER_TRANSLATE_Y }, + duration: 200, + useNativeDriver: true, + }).start(); + this.scrollView.scrollTo({ + y: currentScrollPosition + heightToScroll - remainder, + }); + } else { + this.scrollView.scrollTo({ + y: currentScrollPosition + heightToScroll, + }); + } } } } @@ -295,11 +309,10 @@ class SurveyScreen extends React.Component { .collection('survey') .doc(surveyId) .collection('dataPoints') - .add(_markerToDataPoint(marker)) + .add(marker) .then(doc => { - const { id, timestamp } = doc; + const { id } = doc; marker.id = id; - marker.timestamp = timestamp; markersCopy.push(marker); this.setState({ markers: markersCopy, activeMarkerId: id }, this.resetDrawer); }); @@ -332,6 +345,11 @@ class SurveyScreen extends React.Component { render() { const { activeMarkerId, markers } = this.state; const activeMarker = _.find(markers, { id: activeMarkerId }); + const note = _.get(activeMarker, 'note', ''); + const noteButtonLabel = note ? 'Edit note' : 'Add note'; + const direction = this.getToggleDirection(); + const chevronIconName = `ios-arrow-${direction}`; + return ( - + - {activeMarkerId ? ( - - ) : ( - + {activeMarker && ( + + + + + + {activeMarker.title} + {activeMarker.dateLabel} + + + + + )} - + {!activeMarker && } + {activeMarker && ( - + + + + + )} + {this.state.modalVisible && ( + { + this.setFormResponse(activeMarker.id, 'note', note, 0); + this.setState({ modalVisible: false }); + }} + /> + )} ); } @@ -411,9 +463,31 @@ const styles = StyleSheet.create({ drawerHeader: { alignSelf: 'stretch', }, + headerContent: { + flexDirection: 'row', + alignItems: 'center', + }, + titleContainer: { paddingVertical: 10, paddingHorizontal: 20 }, + title: { fontWeight: 'bold' }, + personIconWrapper: { + padding: 12, + justifyContent: 'center', + alignItems: 'center', + }, formContainer: { alignSelf: 'stretch', }, + drawerFooter: { + padding: 10, + flexDirection: 'row', + }, + greyButton: { + width: Layout.window.width, + flexShrink: 1, + backgroundColor: '#FAFAFA', + borderWidth: 1, + borderColor: 'rgba(0, 0, 0, 0.12)', + }, bottomGuard: { // This view adds whitespace below the drawer, in case the user over-pans it position: 'absolute', @@ -431,6 +505,10 @@ const styles = StyleSheet.create({ backgroundColor: '#D8D8D8', borderRadius: 10, }, + chevron: { + position: 'absolute', + right: 20, + }, instructionsContainer: { display: 'flex', flexDirection: 'row',