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 (
+
+
+
+ );
+ }
+}
+
+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 }) => (
+
+ }>
+
+
+
+
+
+);
+
+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'
- }
-};