Connection Notes (CRUD)

This commit is contained in:
Omar 2019-01-30 01:23:21 -08:00
parent 588f9cc92d
commit 605b0557c4
8 changed files with 377 additions and 6 deletions

45
package-lock.json generated
View File

@ -7479,9 +7479,9 @@
}
},
"moment": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz",
"integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8="
"version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"morgan": {
"version": "1.9.1",
@ -8369,6 +8369,14 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.0.tgz",
"integrity": "sha512-sluvZZ1YiTLD5jsqZcDmFyV2EwToyXZBfpoVOmktMmW+VEnhgakFHnasVph65fOjGPTWN0Nw3+XQaSeMayr0kg=="
},
"raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"requires": {
"performance-now": "^2.1.0"
}
},
"randomatic": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
@ -8633,6 +8641,13 @@
"requires": {
"moment": "2.19.3",
"tinymask": "^1.0.2"
},
"dependencies": {
"moment": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.19.3.tgz",
"integrity": "sha1-vbmdJw1tf9p4zA+6zoVeJ/59pp8="
}
}
},
"react-native-permissions": {
@ -8660,6 +8675,16 @@
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-1.0.0-alpha.19.tgz",
"integrity": "sha512-+a7GdwzLWYWYVUJMg+XuyBoRFGD8GdGyBfebuTNBY+xwUZpTXCaK/GlLGL6EE3h0iBHZu83do7zViEailWRNyA=="
},
"react-native-swipeout": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/react-native-swipeout/-/react-native-swipeout-2.3.6.tgz",
"integrity": "sha512-t9suUCspzck4vp2pWggWe0frS/QOtX6yYCawHnEes75A7dZCEE74bxX2A1bQzGH9cUMjq6xsdfC94RbiDKIkJg==",
"requires": {
"create-react-class": "^15.6.0",
"prop-types": "^15.5.10",
"react-tween-state": "^0.1.5"
}
},
"react-native-tab-view": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.3.1.tgz",
@ -8803,6 +8828,15 @@
"react-proxy": "^1.1.7"
}
},
"react-tween-state": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/react-tween-state/-/react-tween-state-0.1.5.tgz",
"integrity": "sha1-6YsGZVHvuTy5LdG+FJlcLj3q4zk=",
"requires": {
"raf": "^3.1.0",
"tween-functions": "^1.0.1"
}
},
"read-pkg": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
@ -10533,6 +10567,11 @@
"safe-buffer": "^5.0.1"
}
},
"tween-functions": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz",
"integrity": "sha1-GuOlDnxguz3vd06scHrLynO7w/8="
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",

View File

@ -9,6 +9,7 @@
"dependencies": {
"acorn": "^6.0.4",
"i18n-js": "^3.1.0",
"moment": "^2.24.0",
"react": "16.6.3",
"react-native": "0.57.8",
"react-native-app-settings": "^2.0.1",
@ -19,6 +20,7 @@
"react-native-gesture-handler": "^1.0.12",
"react-native-masked-text": "^1.9.2",
"react-native-permissions": "^1.1.1",
"react-native-swipeout": "^2.3.6",
"react-native-vector-icons": "^4.6.0",
"react-native-webview": "^2.14.3",
"react-navigation": "^3.0.9",

View File

@ -0,0 +1,19 @@
import React from 'react';
import { View, TouchableOpacity, Button } from 'react-native';
import { Icon } from 'react-native-elements';
const NotesHeader = ({ keyboardVisible, onBackPress, onDonePress }) => {
const DoneButton = keyboardVisible ? (<Button title='Done' color='#0D497E' onPress={onDonePress} />) : null;
return (
<View style={{ flexDirection: 'row', justifyContent: 'space-between', height: 80, alignItems: 'flex-end' }}>
<TouchableOpacity onPress={onBackPress}>
<Icon name='chevron-left' size={40} color='#0D497E' />
</TouchableOpacity>
{DoneButton}
</View>
);
};
export default NotesHeader;

View File

@ -31,7 +31,23 @@ const endpoints = {
findUsersByPhone: {
method: 'POST',
url: `${baseurl}/users/by/phone`
}
},
notesCreate: (toTagferId) => ({
method: 'PUT',
url: `${baseurl}/notes/me/${toTagferId}`
}),
notesUpdate: (toTagferId) => ({
method: 'POST',
url: `${baseurl}/notes/me/${toTagferId}`
}),
notesDelete: (toTagferId) => ({
method: 'DELETE',
url: `${baseurl}/notes/me/${toTagferId}`
}),
notesAll: (toTagferId) => ({
method: 'GET',
url: `${baseurl}/notes/me/${toTagferId}`
})
};
export default endpoints;

View File

@ -8,8 +8,10 @@ import SignupScreenThreeB from '../screens/auth/SignupScreen3b';
import SignupScreenFive from '../screens/auth/SignupScreen5';
import TwitterWebView from '../screens/auth/TwitterWebView';
import ForgotPasswordScreen from '../screens/auth/ForgotPasswordScreen';
import NotesViewScreen from '../screens/notes/ViewScreen';
import NotesEditScreen from '../screens/notes/EditScreen';
const MainNavigator = createStackNavigator({
const MainNavigator = createStackNavigator({
welcome: WelcomeScreen,
login: LoginScreen,
forgotPassword: ForgotPasswordScreen,
@ -17,7 +19,9 @@ const MainNavigator = createStackNavigator({
signup2: SignupScreenTwo,
signup3a: SignupScreenThreeA,
signup3b: SignupScreenThreeB,
signup5: SignupScreenFive
signup5: SignupScreenFive,
notesView: NotesViewScreen,
notesEdit: NotesEditScreen,
});
const RootNavigator = createStackNavigator(

View File

@ -0,0 +1,103 @@
import React from 'react';
import moment from 'moment';
import { View, Text, TextInput, Keyboard, KeyboardAvoidingView } from 'react-native';
import NotesHeader from '../../components/new/BackDoneHeader';
export default class NotesEditScreen extends React.Component {
static navigationOptions = {
header: null
}
constructor(props) {
super(props);
const { noteId, content, updatedAt } = this.props.navigation.state.params;
const isUpdate = !!noteId;
this.state = {
noteId,
content,
updatedAt,
keyboardVisible: !isUpdate
};
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => this.setState({ keyboardVisible: true }));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => this.setState({ keyboardVisible: false }));
this.onDonePress = this.onDonePress.bind(this);
this.onBackPress = this.onBackPress.bind(this);
this.baseContent = content;
this.isUpdate = isUpdate;
}
componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
// Save the current note status and navigate the user back to the notes view screen
onBackPress() {
this.saveNote();
this.props.navigation.goBack();
}
onDonePress() {
Keyboard.dismiss();
}
// UpdatedAt is in UTC
onChangeText(content) {
this.setState({ content, updatedAt: new Date().getTime() });
}
// Calls the handlers from the main view to create, delete or update a note
saveNote() {
const { noteId, content, updatedAt } = this.state;
if (!content && this.isUpdate) {
this.props.navigation.state.params.onEmptyNote({ noteId });
} else if (this.baseContent !== content) {
this.props.navigation.state.params.onNoteAdded({ noteId, content, updatedAt });
}
}
render() {
return (
<View style={styles.container}>
<NotesHeader keyboardVisible={this.state.keyboardVisible} onDonePress={this.onDonePress} onBackPress={this.onBackPress} />
<View style={styles.dateContainer}>
<Text style={styles.dateText}>{moment(this.state.updatedAt).format('MMMM DD, hh:mm')}</Text>
</View>
<KeyboardAvoidingView style={{ flexGrow: 1 }} behavior='padding' keyboardVerticalOffset={15}>
<TextInput
autoCorrect={false}
autoFocus={!this.isUpdate}
multiline
value={this.state.content}
onChangeText={this.onChangeText.bind(this)}
style={styles.textInput}
placeholder='Your note here'
/>
</KeyboardAvoidingView>
</View>
);
}
}
const styles = {
container: {
flex: 1
},
textInput: {
fontSize: 16,
marginHorizontal: 15,
height: '100%'
},
dateContainer: {
flexDirection: 'row',
justifyContent: 'center'
},
dateText: {
fontSize: 15
}
};

View File

@ -0,0 +1,171 @@
import React from 'react';
import moment from 'moment';
import { View, Text, FlatList, TouchableOpacity } from 'react-native';
import { Divider, Icon } from 'react-native-elements';
import SwipeOut from 'react-native-swipeout';
import { fetchTagferApi } from '../../utils/fetch';
import { showFlashErrorMessage } from '../../utils/errorHandler';
import endpoints from '../../config/apiEndpoints';
export default class NotesViewScreen extends React.Component {
static navigationOptions = {
title: 'Contact\'s Notes',
headerStyle: { borderBottomWidth: 0 },
headerTintColor: '#0D497E'
};
constructor(props) {
super(props);
this.state = {
allNotes: []
};
this.onLoad();
this.pressIndex = -1; // indicates that no note is selected
this.onNoteAdded = this.onNoteAdded.bind(this);
this.onEmptyNote = this.onEmptyNote.bind(this);
}
// Handles calling TagferAPI to create/update a note and updates the view
async onNoteAdded(newNote) {
const toTagferId = this.props.navigation.getParam('tagferId', 'gboyle');
const endpoint = newNote.noteId ? endpoints.notesUpdate(toTagferId) : endpoints.notesCreate(toTagferId);
const resp = await fetchTagferApi(endpoint, newNote);
if (resp.error) {
console.log(resp);
return showFlashErrorMessage(resp.error);
}
// Remove the old note from the view
const { allNotes } = this.state;
if (this.pressIndex !== -1) {
allNotes.splice(this.pressIndex, 1);
}
// Add the new note to the view
this.setState({ allNotes: [{ ...newNote, noteId: resp.noteId }, ...allNotes] });
}
// Handles calling TagferAPI to deleting a note and updates the view
async onEmptyNote(noteId) {
const toTagferId = this.props.navigation.getParam('tagferId', 'gboyle');
const endpoint = endpoints.notesDelete(toTagferId);
const resp = await fetchTagferApi(endpoint, { noteId });
if (resp.error) {
return showFlashErrorMessage(resp.error);
}
// Remove the old note from the view
const { allNotes } = this.state;
if (this.pressIndex !== -1) {
allNotes.splice(this.pressIndex, 1);
}
this.setState({ allNotes: [...allNotes] });
}
// Handles calling TagferAPI and then loads all the notes into the view
async onLoad() {
const toTagferId = this.props.navigation.getParam('tagferId', 'gboyle');
const resp = await fetchTagferApi(endpoints.notesAll(toTagferId));
return resp.error ? showFlashErrorMessage(resp.error) : this.setState({ allNotes: resp.notes });
}
// Updates the pressed indexx and navigates to the edit screen by passing the note's values and the handlers
onNavigateToEditScreen(note, index) {
this.pressIndex = index;
this.props.navigation.navigate('notesEdit', { ...note, onNoteAdded: this.onNoteAdded, onEmptyNote: this.onEmptyNote });
}
renderItem(note, index) {
return (
<NoteListItem
note={note}
onItemPress={() => this.onNavigateToEditScreen(note, index)}
onDeletePress={() => { this.pressIndex = index; this.onEmptyNote(note.noteId); }}
/>
);
}
render() {
return (
<View style={{ flex: 1 }}>
<TouchableOpacity style={styles.addNoteButtonContainer} onPress={() => this.onNavigateToEditScreen({ content: '' }, -1)}>
<Text style={styles.addNoteButtonText}>ADD NOTE</Text>
</TouchableOpacity>
<FlatList
data={this.state.allNotes}
extraData={this.state}
renderItem={({ item, index }) => this.renderItem(item, index)}
keyExtractor={(item) => item.noteId}
style={styles.notesFlatList}
/>
</View>
);
}
}
// Custom internal notes component (not resusable)
const NoteListItem = ({ note, onItemPress, onDeletePress }) => {
const dateFormat = 'MM/DD/YYYY';
const date = moment(note.updatedAt).format(dateFormat);
return (
<SwipeOut autoClose backgroundColor='transparent' right={DeleteSwiperProps(onDeletePress)} >
<TouchableOpacity onPress={onItemPress}>
<Text style={styles.noteDate}>{date}</Text>
<Text style={styles.noteContent} numberOfLines={1}>{note.content}</Text>
<Text style={styles.noteViewButton}>READ MORE</Text>
</TouchableOpacity>
<Divider />
</SwipeOut>
);
};
// Internal props of the swipe to delete button within the note list item
const DeleteSwiperProps = (onDeletePress) => ([
{
type: 'delete',
component: <Icon name='delete' color='white' size={36} containerStyle={styles.deleteIcon} />,
onPress: onDeletePress
}
]);
const styles = {
addNoteButtonContainer: {
alignItems: 'flex-end',
marginRight: 10
},
addNoteButtonText: {
fontSize: 15,
color: '#53ACF0'
},
noteDate: {
fontSize: 13,
color: '#6A6A6A',
marginBottom: 7,
marginTop: 10
},
noteContent: {
fontSize: 15
},
noteViewButton: {
fontSize: 13,
color: '#0462AA',
textAlign: 'right',
margin: 3
},
notesFlatList: {
flex: 1,
marginHorizontal: 10,
marginTop: 10
},
deleteIcon: {
flex: 1,
alignItems: 'center'
}
};

View File

@ -1,4 +1,5 @@
import appConfig from '../config/appConfig.json';
import errors from '../config/errors.js';
export function fetchAuth(endpoint, body) {
const request = {
@ -12,3 +13,19 @@ export function fetchAuth(endpoint, body) {
return fetch(endpoint.url, request).then(res => res.json());
}
const sessionId = 'df5e641b-2e10-4f2d-820b-7b35970b88eb'; // TODO: THIS WILL BE REPLACED BY UPS1
// Fetch wrapper for authorized calls with valid session id. Returns either a JSON object documented in the wiki or an error.
export function fetchTagferApi(endpoint, body) {
const request = {
method: endpoint.method,
headers: {
Authorization: sessionId,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
};
return fetch(endpoint.url, request).then(res => res.json()).catch(() => ({ error: errors.APP_NETWORK_ERROR }));
}