From f30b457e4878796bcd2c6d62ac0a0b99435a4753 Mon Sep 17 00:00:00 2001 From: Omar Date: Thu, 7 Mar 2019 01:04:02 -0800 Subject: [PATCH] UPS8-View own profile --- ios/Tagfer/Info.plist | 2 +- package.json | 1 + src/components/navigators/AppNavigator.js | 8 +- .../navigators/ProfileTabNavigator.js | 37 ++++ src/components/profile/ActionBar.js | 84 ++++++++ src/components/profile/EditButton.js | 11 + src/components/profile/InfoBar.js | 25 +++ src/components/profile/MainContent.js | 196 ++++++++++++++++++ src/components/profile/OptionsMenu.js | 28 +++ src/components/profile/ProfileHeader.js | 82 ++++++++ src/config/apiEndpoints.js | 20 +- src/screens/profile/EventsScreen.js | 21 ++ src/screens/profile/ProfileScreen.js | 142 +++++++++++-- 13 files changed, 638 insertions(+), 19 deletions(-) create mode 100644 src/components/navigators/ProfileTabNavigator.js create mode 100644 src/components/profile/ActionBar.js create mode 100644 src/components/profile/EditButton.js create mode 100644 src/components/profile/InfoBar.js create mode 100644 src/components/profile/MainContent.js create mode 100644 src/components/profile/OptionsMenu.js create mode 100644 src/components/profile/ProfileHeader.js create mode 100644 src/screens/profile/EventsScreen.js diff --git a/ios/Tagfer/Info.plist b/ios/Tagfer/Info.plist index 67049b8..ff67676 100644 --- a/ios/Tagfer/Info.plist +++ b/ios/Tagfer/Info.plist @@ -21,7 +21,7 @@ CFBundleSignature ???? CFBundleVersion - 3 + 5 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/package.json b/package.json index 98935fb..ec7e792 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/navigators/AppNavigator.js b/src/components/navigators/AppNavigator.js index e3bdd7b..d7e3253 100644 --- a/src/components/navigators/AppNavigator.js +++ b/src/components/navigators/AppNavigator.js @@ -1,11 +1,11 @@ import EventsScreen from '../../screens/events/EventsScreen'; import ConnectScreen from '../../screens/connections/ConnectScreen'; import MessagesScreen from '../../screens/messages/MessagesScreen'; -import ProfileScreen from '../../screens/profile/ProfileScreen'; import BottomTabNavigator from './BottomTabNavigator'; import TopStackNavigator from './TopStackNavigator'; import ContactsTabNavigator from './ContactsTabNavigator'; +import ProfileTabNavigator from './ProfileTabNavigator'; const ConnectionsStack = TopStackNavigator({ connect: ConnectScreen @@ -15,10 +15,14 @@ const ContactsStack = TopStackNavigator({ contcats: ContactsTabNavigator }); +const ProfileStack = TopStackNavigator({ + profile: ProfileTabNavigator +}); + export default BottomTabNavigator({ Events: EventsScreen, Contacts: ContactsStack, Connections: ConnectionsStack, Messages: MessagesScreen, - Profile: ProfileScreen + Profile: ProfileStack }); diff --git a/src/components/navigators/ProfileTabNavigator.js b/src/components/navigators/ProfileTabNavigator.js new file mode 100644 index 0000000..c34aa26 --- /dev/null +++ b/src/components/navigators/ProfileTabNavigator.js @@ -0,0 +1,37 @@ +import { createMaterialTopTabNavigator } from 'react-navigation'; + +import ProfileScreen from '../../screens/profile/ProfileScreen'; +import EventsScreen from '../../screens/profile/EventsScreen'; + +const profileTabBarOptions = { + upperCaseLabel: false, + activeTintColor: 'white', + inactiveTintColor: '#53ACF0', + lazy: true, + labelStyle: { + margin: 1, + fontSize: 13, + }, + tabStyle: { + borderColor: '#53ACF0', + borderWidth: 1, + padding: 5 + }, + indicatorStyle: { + backgroundColor: '#53ACF0', + bottom: 0, + height: 50, + }, + style: { + backgroundColor: 'transparent', + } +}; + +export default createMaterialTopTabNavigator( + { + 'Basic Info': ProfileScreen, + Events: EventsScreen + }, + { + tabBarOptions: profileTabBarOptions + }); diff --git a/src/components/profile/ActionBar.js b/src/components/profile/ActionBar.js new file mode 100644 index 0000000..21a7b7f --- /dev/null +++ b/src/components/profile/ActionBar.js @@ -0,0 +1,84 @@ +import React from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { Icon } from 'react-native-elements'; + +const ActionBar = ({ status, isSelf }) => { + if (isSelf) { return null; } + if (status === 'connected') { return ; } + if (status === 'pending') { return ; } + + return ; +}; + +const NotConnectedBar = ({ isPending }) => ( + + + + Share + + {isPending ? : } + + + QR Code + + +); + +const ConnectedBar = () => ( + + + + + + + + + + + + + + +); + +const PendingBtn = () => ( + + + Pending + +); + +const ConnectBtn = () => ( + + + Connect + +); + +const styles = { + actionBtn: { + flex: 1, + paddingVertical: 6, + alignItems: 'center', + justifyContent: 'center', + borderWidth: 0.5, + borderColor: '#E0E3EF', + flexDirection: 'row' + }, + actionBtnTitle: { + marginLeft: 7, + color: '#53ACF0', + fontSize: 14 + }, + pedningBtnTitle: { + marginLeft: 7, + color: '#008A1C', + fontSize: 14 + }, + connectedBtns: { + marginHorizontal: 15, + marginVertical: 5 + } +}; + +export default ActionBar; diff --git a/src/components/profile/EditButton.js b/src/components/profile/EditButton.js new file mode 100644 index 0000000..4b0b2a4 --- /dev/null +++ b/src/components/profile/EditButton.js @@ -0,0 +1,11 @@ +import React from 'react'; +import { TouchableOpacity } from 'react-native'; +import { Icon } from 'react-native-elements'; + +const EditButton = ({ size }) => ( + + + +); + +export default EditButton; diff --git a/src/components/profile/InfoBar.js b/src/components/profile/InfoBar.js new file mode 100644 index 0000000..12fe490 --- /dev/null +++ b/src/components/profile/InfoBar.js @@ -0,0 +1,25 @@ +import React from 'react'; +import { View, Text } from 'react-native'; +import { Icon } from 'react-native-elements'; + +const InfoBar = ({ numOfContacts, numOfRecs }) => ( + + + + + Tagfer Top Networker + + + + {numOfRecs} Recommendations + + + + + {numOfContacts} + Contacts + + +); + +export default InfoBar; diff --git a/src/components/profile/MainContent.js b/src/components/profile/MainContent.js new file mode 100644 index 0000000..ad0ef9a --- /dev/null +++ b/src/components/profile/MainContent.js @@ -0,0 +1,196 @@ +import React from 'react'; +import moment from 'moment'; +import { View, Text, Linking, Platform } from 'react-native'; +import { Icon, Divider } from 'react-native-elements'; +import EditButton from './EditButton'; + +const MainContent = ({ about, help, need, experienceList, educationList, contact, canEdit }) => { + const contactsList = createContactsList(contact); + + return ( + + {/* ABOUT SECTION */} + {renderSingleSection('About', about, canEdit)} + + {/* HELP SECTION */} + {renderSingleSection('How can you help your contacts?', help, canEdit)} + + {/* NEED SECTION */} + {renderSingleSection('What do you need?', need, canEdit)} + + {/* EXPERIENCE SECTION */} + {renderMultipleSections('Experience', experienceList, (e) => , canEdit)} + + {/* EDUCATION SECTION */} + {renderMultipleSections('Education', educationList, (e) => , canEdit)} + + {/* CONTACT SECTION */} + {renderMultipleSections('Contact', contactsList, (c) => , canEdit)} + + ); +}; + +function renderSingleSection(title, body, canEdit) { + if (body) { + return ( + +
+ {body} +
+
+ ); + } +} + +function renderMultipleSections(title, list, renderBody, canEdit) { + const length = list.length; + if (length === 0) { return null; } + + const sections = []; + for (let i = 0; i < length; i++) { + const addDivider = i !== length - 1; + const section = ( +
+ {renderBody(list[i])} +
+ ); + sections.push(section); + } + + return ( + + {sections} + + ); +} + +const ExperienceBody = ({ experience }) => { + const { jobTitle, companyName, companyLocation, startDate, endDate, summary } = experience; + + return ( + + {jobTitle} at {companyName} + {companyLocation} + + {summary} + + ); +}; + +const EducationBody = ({ education }) => { + const { school, degree, study, startDate, endDate, summary } = education; + + return ( + + {degree} at {school} + {study} + + {summary} + + ); +}; + +const ContactBody = ({ contact }) => { + const { iconName, contactType, contactValue, linkTo } = contact; + + return ( + + + + {contactType} + linkTo(contactValue)} >{contactValue} + + + ); +}; + +const Duration = ({ startDate, endDate }) => { + if (!startDate && !endDate) { return null; } + const start = moment(startDate).format('MMMM YYYY'); + const end = endDate === -1 ? 'Present' : moment(endDate).format('MMMM YYYY'); + + return ( + {start} - {end} + ); +}; + +const Card = ({ title, children }) => ( + + {title} + {children} + +); + + +const Section = ({ children, addDivider, addEdit }) => ( + + {addEdit ? : } + {children} + {addDivider ? : null} + +); + +function openEmail(email) { + return Linking.canOpenURL(`mailto:${email}`).then(supported => { + if (supported) { + Linking.openURL(`mailto:${email}`); + } + }); +} + +function openPhone(phoneNumber) { + return Linking.canOpenURL(`tel:${phoneNumber}`).then(supported => { + if (supported) { + Linking.openURL(`tel:${phoneNumber}`); + } + }); +} + +function createContactsList(contact = {}) { + const phoneNumber = { + iconName: 'phone', + contactType: 'Phone Number', + contactValue: contact.phoneNumber, + linkTo: openPhone + }; + const email = { + iconName: 'email', + contactType: 'Email', + contactValue: contact.email, + linkTo: openEmail + }; + const contacts = []; + if (contact.email) { + contacts.push(email); + } + + if (contact.phoneNumber) { + contacts.push(phoneNumber); + } + + return contacts; +} + +const styles = { + card: { + width: '95%', + padding: 10, + margin: 5, + borderWidth: 0.3, + borderColor: '#e1e8ee', + ...Platform.select({ + android: { + elevation: 1, + }, + default: { + shadowColor: 'rgba(0,0,0, .2)', + shadowOffset: { height: 0, width: 0 }, + shadowOpacity: 1, + shadowRadius: 1, + }, + }), + backgroundColor: 'white' + } +}; + +export default MainContent; diff --git a/src/components/profile/OptionsMenu.js b/src/components/profile/OptionsMenu.js new file mode 100644 index 0000000..2c4cb7d --- /dev/null +++ b/src/components/profile/OptionsMenu.js @@ -0,0 +1,28 @@ +import React from 'react'; +import { View, TouchableOpacity } from 'react-native'; +import { Icon } from 'react-native-elements'; +import Menu, { MenuItem, MenuDivider } from 'react-native-material-menu'; + +const OptionsButton = ({ onPress }) => ( + + + +); + +const OptionsMenu = ({ setMenu, showMenu, onPress }) => ( + + }> + Show QR Code + + Share Profile + + +); + +const styles = { + optionsMenu: { + marginTop: 10 + } +}; + +export default OptionsMenu; diff --git a/src/components/profile/ProfileHeader.js b/src/components/profile/ProfileHeader.js new file mode 100644 index 0000000..2722e29 --- /dev/null +++ b/src/components/profile/ProfileHeader.js @@ -0,0 +1,82 @@ +import React from 'react'; +import { View, ScrollView, Text, Animated, Dimensions } from 'react-native'; +import { Icon } from 'react-native-elements'; + +import TagferAvatar from '../new/TagferAvatar'; +import EditButton from './EditButton'; +import OptionsMenu from './OptionsMenu'; + +const SCREEN_WIDTH = Dimensions.get('window').width; +const SCROLL_X = new Animated.Value(0); + +const ProfileHeader = ({ photoURL, fullName, tagferId, jobTitle, companyName, companyLocation, setOptionsMenu, showOptionsMenu }) => { + const onScroll = Animated.event( + [{ nativeEvent: { contentOffset: { x: SCROLL_X } } }] + ); + + return ( + + + {/* SWIPE SCREEN 1 */} + + {/* BUTTONS SECTION */} + + + + + + + {/* PROFILE CONTENT SECTION */} + + + {fullName} + @{tagferId} + {jobTitle} at {companyName} + {companyLocation} + + + + {/* SWIPE SCREEN 2 */} + + + + + + + + ); +}; + +const SwiperDots = () => { + const position = Animated.divide(SCROLL_X, SCREEN_WIDTH); + const opacity = (i) => position.interpolate({ + inputRange: [i - 1, i, i + 1], + outputRange: [0.3, 1, 0.3], + extrapolate: 'clamp' + }); + + return ( + + + + + + ); +}; + +export default ProfileHeader; diff --git a/src/config/apiEndpoints.js b/src/config/apiEndpoints.js index 0e9d418..1c26a95 100644 --- a/src/config/apiEndpoints.js +++ b/src/config/apiEndpoints.js @@ -1,4 +1,4 @@ -const baseurl = 'http://localhost:3000'; +const baseurl = 'https://us-central1-tagfer-inc.cloudfunctions.net/api'; const endpoints = { login: { method: 'POST', @@ -59,7 +59,23 @@ const endpoints = { getAllConnections: { method: 'GET', url: `${baseurl}/connections/me` - } + }, + getProfileSelf: (profileN) => ({ + method: 'GET', + url: `${baseurl}/profiles/me/${profileN}` + }), + getProfile: (tagferId) => ({ + method: 'GET', + url: `${baseurl}/profiles/${tagferId}` + }), + getConnectionCountSelf: { + method: 'GET', + url: `${baseurl}/connections/me/count` + }, + getConnectionCount: (tagferId) => ({ + method: 'GET', + url: `${baseurl}/connections/${tagferId}/count` + }) }; export default endpoints; diff --git a/src/screens/profile/EventsScreen.js b/src/screens/profile/EventsScreen.js new file mode 100644 index 0000000..34f948d --- /dev/null +++ b/src/screens/profile/EventsScreen.js @@ -0,0 +1,21 @@ +import React from 'react'; +import { Text, View } from 'react-native'; + +export default class AllConnectionsScreen extends React.Component { + + render() { + return ( + + Events + + ); + } +} + +const styles = { + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + } +}; diff --git a/src/screens/profile/ProfileScreen.js b/src/screens/profile/ProfileScreen.js index a0de966..ee7cba1 100644 --- a/src/screens/profile/ProfileScreen.js +++ b/src/screens/profile/ProfileScreen.js @@ -1,21 +1,135 @@ import React from 'react'; -import { Text, View } from 'react-native'; +import { ScrollView } from 'react-native'; + +import ProfileHeader from '../../components/profile/ProfileHeader'; +import MainContent from '../../components/profile/MainContent'; +import ActionBar from '../../components/profile/ActionBar'; +import InfoBar from '../../components/profile/InfoBar'; + +import endpoints from '../../config/apiEndpoints'; + +import { fetchTagferApi } from '../../utils/fetch'; +import { showFlashErrorMessage } from '../../utils/errorHandler'; + +export default class ProfileScreen extends React.Component { + + constructor(props) { + super(props); + // FIXME: Hardcoded data needs to be removed + this.state = { + profile: { + fullName: 'Ruth Ougston', + tagferId: 'ruthoug', + experience: [{ + jobTitle: 'CTO', + companyName: 'Square', + companyLocation: 'California, USA', + startDate: 1488904944000, + endDate: -1, + summary: 'Company name is a duis nec sapien convallis, tincidunt orci sed, ultrices augue. Quisque ornare non erat vitae eleifend. ' + }, + { + jobTitle: 'Software Engineer', + companyName: 'Google', + companyLocation: 'California, USA', + startDate: 1446914544000, + endDate: 1399477344000, + summary: 'Company name is a duis nec sapien convallis, tincidunt orci sed, ultrices augue. Quisque ornare non erat vitae eleifend. ' + }], + education: [{ + degree: 'Bachelor\'s Degree', + school: 'Harvard', + study: 'Computer Engineering', + startDate: 1283874144000, + endDate: 1399477344000, + summary: 'Harvard is a duis nec sapien convallis, tincidunt orci sed, ultrices augue. Quisque ornare non erat vitae eleifend. ' + }], + contact: { + email: 'ruthoug@google.com', + phoneNumber: '+1 404 569-(9123)' + } + }, + numOfRecs: 12, + numOfContacts: 500 + }; + + this.setOptionsMenu = this.setOptionsMenu.bind(this); + this.showOptionsMenu = this.showOptionsMenu.bind(this); + this.tagferId = this.props.navigation.getParam('tagferId'); + this.profileN = 1; + this.isSelf = !this.tagferId; + } + + componentDidMount() { + // this.onLoad1(); + // this.onLoad2(); + } + + async onLoad1() { + const endpoint = this.isSelf ? endpoints.getProfileSelf(this.profileN) : endpoints.getProfile(this.tagferId); + const response = await fetchTagferApi(endpoint); + if (response.error) { + return showFlashErrorMessage(response.error); + } + + // FIXME: Make the backend aggregate the data in the same format as the state at the top, this requires a model change. + // FIXME: Need to change the behaviour of how information is saved. + const { profile } = response; + + this.setState({ profile }); + } + + async onLoad2() { + const endpoint = this.isSelf ? endpoints.getConnectionCountSelf : endpoints.getConnectionCount(this.tagferId); + const response = await fetchTagferApi(endpoint); + if (response.error) { + return showFlashErrorMessage(response.error); + } + + this.setState({ numOfContacts: response.count }); + } + + getExperience(attribute) { + if (this.state.profile.experience.length !== 0) { + return this.state.profile.experience[0][attribute]; + } + } + + setOptionsMenu(ref) { + this.sortMenu = ref; + } + + showOptionsMenu() { + this.sortMenu.show(); + } -export default class AllConnectionsScreen extends React.Component { - render() { return ( - - Profile - + + + + + + + ); } } - -const styles = { - container: { - flex: 1, - justifyContent: 'center', - alignItems: 'center' - } -};