UPS8-View own profile

This commit is contained in:
Omar 2019-03-07 01:04:02 -08:00
parent 6fd3cc46de
commit f30b457e48
13 changed files with 638 additions and 19 deletions

View File

@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>3</string>
<string>5</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>

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",

View File

@ -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
});

View File

@ -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
});

View File

@ -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 <ConnectedBar />; }
if (status === 'pending') { return <NotConnectedBar isPending />; }
return <NotConnectedBar />;
};
const NotConnectedBar = ({ isPending }) => (
<View style={{ flexDirection: 'row', justifyContent: 'space-around', marginTop: 20, backgroundColor: '#F5F8FA' }}>
<TouchableOpacity style={styles.actionBtn}>
<Icon type='material-community' name='share' color='#53ACF0' />
<Text style={styles.actionBtnTitle}>Share</Text>
</TouchableOpacity>
{isPending ? <PendingBtn /> : <ConnectBtn />}
<TouchableOpacity style={styles.actionBtn}>
<Icon type='material-community' name='qrcode' color='#53ACF0' />
<Text style={styles.actionBtnTitle}>QR Code</Text>
</TouchableOpacity>
</View>
);
const ConnectedBar = () => (
<View style={{ flexDirection: 'row', justifyContent: 'center', marginTop: 20, borderWidth: 0.5, borderColor: '#E0E3EF' }}>
<TouchableOpacity style={styles.connectedBtns}>
<Icon type='material' name='call' color='#008A1C' size={32} />
</TouchableOpacity>
<TouchableOpacity style={styles.connectedBtns}>
<Icon type='material-community' name='share' color='#53ACF0' size={32} />
</TouchableOpacity>
<TouchableOpacity style={styles.connectedBtns}>
<Icon type='material-community' name='message-text' color='#0462AA' size={32} />
</TouchableOpacity>
<TouchableOpacity style={styles.connectedBtns}>
<Icon type='material-community' name='file-document' color='#F3DD0F' size={32} />
</TouchableOpacity>
</View>
);
const PendingBtn = () => (
<TouchableOpacity style={styles.actionBtn}>
<Icon type='material' name='person-add' color='#008A1C' />
<Text style={styles.pedningBtnTitle}>Pending</Text>
</TouchableOpacity>
);
const ConnectBtn = () => (
<TouchableOpacity style={styles.actionBtn}>
<Icon type='material' name='person-add' color='#53ACF0' />
<Text style={styles.actionBtnTitle}>Connect</Text>
</TouchableOpacity>
);
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;

View File

@ -0,0 +1,11 @@
import React from 'react';
import { TouchableOpacity } from 'react-native';
import { Icon } from 'react-native-elements';
const EditButton = ({ size }) => (
<TouchableOpacity style={{ alignItems: 'flex-end' }}>
<Icon name='mode-edit' color='#2B3550' size={size || 20} />
</TouchableOpacity>
);
export default EditButton;

View File

@ -0,0 +1,25 @@
import React from 'react';
import { View, Text } from 'react-native';
import { Icon } from 'react-native-elements';
const InfoBar = ({ numOfContacts, numOfRecs }) => (
<View style={{ flexDirection: 'row', borderWidth: 0.5, borderColor: '#E0E3EF' }}>
<View style={{ flex: 2, alignItems: 'flex-start', paddingVertical: 10, paddingLeft: 5, borderWidth: 0.5, borderColor: '#E0E3EF' }}>
<View style={{ flexDirection: 'row' }}>
<Icon name='star-circle' type='material-community' color='#FFD166' size={17} />
<Text style={{ marginLeft: 3 }}>Tagfer Top Networker</Text>
</View>
<View style={{ flexDirection: 'row' }}>
<Icon name='md-ribbon' type='ionicon' color='#36CCDA' size={20} />
<Text style={{ marginLeft: 5 }}>{numOfRecs} Recommendations</Text>
</View>
</View>
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', borderWidth: 0.5, borderColor: '#E0E3EF', paddingVertical: 10 }}>
<Text style={{ fontSize: 18, fontWeight: '600' }}>{numOfContacts}</Text>
<Text style={{ fontSize: 15, color: '#535353' }}>Contacts</Text>
</View>
</View>
);
export default InfoBar;

View File

@ -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 (
<View style={{ height: '100%', backgroundColor: '#F5F8FA', alignItems: 'center', paddingVertical: 15 }}>
{/* 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) => <ExperienceBody experience={e} />, canEdit)}
{/* EDUCATION SECTION */}
{renderMultipleSections('Education', educationList, (e) => <EducationBody education={e} />, canEdit)}
{/* CONTACT SECTION */}
{renderMultipleSections('Contact', contactsList, (c) => <ContactBody contact={c} />, canEdit)}
</View>
);
};
function renderSingleSection(title, body, canEdit) {
if (body) {
return (
<Card title={title}>
<Section addEdit={canEdit}>
<Text>{body}</Text>
</Section>
</Card>
);
}
}
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 = (
<Section key={i} addEdit={canEdit} addDivider={addDivider}>
{renderBody(list[i])}
</Section>
);
sections.push(section);
}
return (
<Card title={title}>
{sections}
</Card>
);
}
const ExperienceBody = ({ experience }) => {
const { jobTitle, companyName, companyLocation, startDate, endDate, summary } = experience;
return (
<View>
<Text>{jobTitle} at {companyName}</Text>
<Text>{companyLocation}</Text>
<Duration startDate={startDate} endDate={endDate} />
<Text style={{ marginTop: 10 }}>{summary}</Text>
</View>
);
};
const EducationBody = ({ education }) => {
const { school, degree, study, startDate, endDate, summary } = education;
return (
<View>
<Text>{degree} at {school}</Text>
<Text>{study}</Text>
<Duration startDate={startDate} endDate={endDate} />
<Text style={{ marginTop: 10 }}>{summary}</Text>
</View>
);
};
const ContactBody = ({ contact }) => {
const { iconName, contactType, contactValue, linkTo } = contact;
return (
<View style={{ flexDirection: 'row' }}>
<Icon name={iconName} />
<View style={{ flexDirection: 'column', marginLeft: 15 }}>
<Text style={{ fontSize: 15, marginBottom: 3 }}>{contactType}</Text>
<Text style={{ color: '#0462AA' }} onPress={() => linkTo(contactValue)} >{contactValue}</Text>
</View>
</View>
);
};
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 (
<Text style={{ color: '#535353' }}>{start} - {end}</Text>
);
};
const Card = ({ title, children }) => (
<View style={styles.card}>
<Text style={{ color: '#2B3550', fontSize: 17, fontWeight: '500' }}>{title}</Text>
{children}
</View>
);
const Section = ({ children, addDivider, addEdit }) => (
<View>
{addEdit ? <EditButton /> : <View style={{ marginTop: 10 }} />}
{children}
{addDivider ? <Divider style={{ marginVertical: 10 }} /> : null}
</View>
);
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;

View File

@ -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 }) => (
<TouchableOpacity onPress={onPress} >
<Icon type='entypo' name='dots-three-vertical' size={24} iconStyle={{ textAlign: 'left' }} color='#535353' />
</TouchableOpacity>
);
const OptionsMenu = ({ setMenu, showMenu, onPress }) => (
<View style={styles.optionsMenu}>
<Menu ref={setMenu} button={<OptionsButton onPress={showMenu} />}>
<MenuItem onPress={onPress} style={{ height: 50 }}>Show QR Code</MenuItem>
<MenuDivider />
<MenuItem onPress={onPress} style={{ height: 50 }}>Share Profile</MenuItem>
</Menu>
</View>
);
const styles = {
optionsMenu: {
marginTop: 10
}
};
export default OptionsMenu;

View File

@ -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 (
<View>
<ScrollView
horizontal
pagingEnabled
showsHorizontalScrollIndicator={false}
onScroll={onScroll}
scrollEventThrottle={16}
>
{/* SWIPE SCREEN 1 */}
<View style={{ width: SCREEN_WIDTH }}>
{/* BUTTONS SECTION */}
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<OptionsMenu
setMenu={setOptionsMenu}
showMenu={showOptionsMenu}
/>
<View style={{ marginTop: 8, marginRight: 5 }}>
<EditButton size={24} />
</View>
</View>
{/* PROFILE CONTENT SECTION */}
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<TagferAvatar size='large' color='#53ACF060' photoURL={photoURL} />
<Text style={{ fontSize: 22, fontWeight: '600', marginTop: 5 }}>{fullName}</Text>
<Text style={{ fontSize: 18, marginBottom: 8, fontWeight: '400' }}>@{tagferId}</Text>
<Text style={{ fontSize: 14 }}>{jobTitle} at {companyName}</Text>
<Text style={{ fontSize: 14, marginBottom: 20, color: '#535353' }}>{companyLocation}</Text>
</View>
</View>
{/* SWIPE SCREEN 2 */}
<View style={{ width: SCREEN_WIDTH, alignItems: 'center', marginVertical: 10 }}>
<Icon type='font-awesome' name='id-card' size={180} color='#53ACF040' />
</View>
</ScrollView>
<SwiperDots />
</View>
);
};
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 (
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center' }}>
<Animated.View
key={'swiperDot0'}
style={{ opacity: opacity(0), height: 7, width: 7, backgroundColor: '#595959', margin: 3, borderRadius: 3 }}
/>
<Animated.View
key={'swiperDot1'}
style={{ opacity: opacity(1), height: 7, width: 7, backgroundColor: '#595959', margin: 3, borderRadius: 3 }}
/>
</View>
);
};
export default ProfileHeader;

View File

@ -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;

View File

@ -0,0 +1,21 @@
import React from 'react';
import { Text, View } from 'react-native';
export default class AllConnectionsScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text>Events</Text>
</View>
);
}
}
const styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
};

View File

@ -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 (
<View style={styles.container}>
<Text>Profile</Text>
</View>
<ScrollView style={{ flex: 1 }}>
<ProfileHeader
fullName={this.state.profile.fullName}
tagferId={this.state.profile.tagferId}
photoURL={this.state.profile.photoURL}
jobTitle={this.getExperience('jobTitle')}
companyName={this.getExperience('companyName')}
companyLocation={this.getExperience('companyLocation') || 'USA'}
setOptionsMenu={this.setOptionsMenu}
showOptionsMenu={this.showOptionsMenu}
canEdit={this.isSelf}
/>
<ActionBar isSelf={this.isSelf} />
<InfoBar numOfRecs={this.state.numOfRecs} numOfContacts={this.state.numOfContacts} />
<MainContent
need={this.state.profile.need}
help={this.state.profile.help}
about={this.state.profile.about}
experienceList={this.state.profile.experience}
educationList={this.state.profile.education}
contact={this.state.profile.contact}
canEdit={this.isSelf}
/>
</ScrollView>
);
}
}
const styles = {
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
};