mirror of
https://bitbucket.org/tagfer_team/tagfer-app.git
synced 2025-12-25 03:37:41 +00:00
Connection Notes (CRUD)
This commit is contained in:
parent
588f9cc92d
commit
605b0557c4
45
package-lock.json
generated
45
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
19
src/components/new/BackDoneHeader.js
Normal file
19
src/components/new/BackDoneHeader.js
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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(
|
||||
|
||||
103
src/screens/notes/EditScreen.js
Normal file
103
src/screens/notes/EditScreen.js
Normal 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
|
||||
}
|
||||
};
|
||||
171
src/screens/notes/ViewScreen.js
Normal file
171
src/screens/notes/ViewScreen.js
Normal 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'
|
||||
}
|
||||
};
|
||||
@ -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 }));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user