Commit aee77d62 authored by Wietse Kuipers's avatar Wietse Kuipers
Browse files

Merge branch 'feature/8-profile-view' into 'master'

Created profile view

Closes #8

See merge request !43
parents fe0e3aec 14c0f478
......@@ -11,3 +11,5 @@ export const LOADEVENTSUCCESS = 'LOADEVENTSUCCESS';
export const LOADEVENTFAILURE = 'LOADEVENTFAILURE';
export const RESETLOGINSTATE = 'RESETLOGINSTATE';
export const WELCOME = 'WELCOME';
export const LOADPROFILESUCCESS = 'LOADPROFILESUCCESS';
export const LOADPROFILEFAILURE = 'LOADPROFILEFAILURE';
import * as types from './actionTypes';
import { navigate } from './navigation';
import { apiUrl } from '../url';
export function success(profile) {
return {
type: types.LOADPROFILESUCCESS,
profile,
};
}
export function fail() {
return {
type: types.LOADPROFILEFAILURE,
};
}
export function loadProfile(token, member = 'me') {
return (dispatch) => {
const data = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
};
return fetch(`${apiUrl}/members/${member}/`, data)
.then(
response => response.json(),
)
.then(
(response) => {
dispatch(success(response));
dispatch(navigate('profile'));
},
)
.catch(
() => {
dispatch(fail());
dispatch(navigate('profile'));
},
);
};
}
import React from 'react';
import PropTypes from 'prop-types';
import { Image, Text, ViewPropTypes, StyleSheet } from 'react-native';
import { Image, Text, ViewPropTypes, StyleSheet, TouchableHighlight } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { connect } from 'react-redux';
import { url } from '../url';
import styles from './style/memberView';
import SquareView from './SquareView';
import { loadProfile } from '../actions/profile';
const regex = new RegExp(`^(${url}/media/public)/(avatars/[^\\.]+)\\.(jpg|jpeg|png|gif)`);
......@@ -19,10 +21,15 @@ const MemberView = (props) => {
return (
<SquareView style={props.style}>
<TouchableHighlight
style={styles.image}
onPress={() => props.loadProfile(props.token, props.member.member)}
>
<Image style={styles.image} source={{ uri: photo }}>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} />
<Text style={styles.nameText}>{props.member.name}</Text>
</Image>
</TouchableHighlight>
</SquareView>
);
};
......@@ -31,8 +38,11 @@ MemberView.propTypes = {
member: PropTypes.shape({
name: PropTypes.string,
photo: PropTypes.string,
member: PropTypes.number,
}).isRequired,
style: ViewPropTypes.style,
token: PropTypes.string.isRequired,
loadProfile: PropTypes.func.isRequired,
};
const defaultStyles = StyleSheet.create({
......@@ -42,4 +52,12 @@ MemberView.defaultProps = {
style: defaultStyles,
};
export default connect(() => ({}), () => ({}))(MemberView);
const mapStateToProps = state => ({
token: state.session.token,
});
const mapDispatchToProps = dispatch => ({
loadProfile: (token, pk) => dispatch(loadProfile(token, pk)),
});
export default connect(mapStateToProps, mapDispatchToProps)(MemberView);
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ScrollView, Text, View, Animated, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import Icon from 'react-native-vector-icons/MaterialIcons';
import LinearGradient from 'react-native-linear-gradient';
import Moment from 'moment';
import { back } from '../actions/navigation';
import styles, { HEADER_MIN_HEIGHT, HEADER_MAX_HEIGHT, HEADER_SCROLL_DISTANCE } from './style/profile';
const getDescription = profile => ([
<Text style={[styles.sectionHeader, styles.marginTop]} key="title">{`Over ${profile.display_name}`}</Text>,
<View style={styles.card} key="content">
<Text
style={[
styles.data,
styles.item,
!profile.profile_description && styles.italics]}
>{profile.profile_description || 'Dit lid heeft nog geen beschrijving geschreven'}</Text>
</View>,
]);
const getPersonalInfo = (profile) => {
const profileInfo = {
starting_year: {
title: 'Cohort',
display: x => x,
},
programme: {
title: 'Studie',
display: x => (x === 'computingscience' ? 'Computing science' : 'Information sciences'),
},
website: {
title: 'Website',
display: x => x,
},
birthday: {
title: 'Verjaardag',
display: x => Moment(x).format('D MMMM YYYY'),
},
};
const profileData = Object.keys(profileInfo).map((key) => {
if (profile[key]) {
return {
title: profileInfo[key].title,
value: profileInfo[key].display(profile[key]),
};
}
return null;
}).filter(n => n);
if (profileData) {
return [
<Text style={styles.sectionHeader} key="title">Persoonlijke gegevens</Text>,
<View style={styles.card} key="content">
{profileData.map((item, i) => (
<View style={[styles.item, i !== 0 && styles.borderTop]} key={item.title}>
<Text style={styles.description}>{item.title}</Text>
<Text style={styles.data}>{item.value}</Text>
</View>
))}
</View>,
];
}
return <View />;
};
const getAchievements = (profile) => {
if (profile.achievements) {
return [
<Text style={styles.sectionHeader} key="title">Verdiensten voor Thalia</Text>,
<View style={styles.card} key="content">
{profile.achievements.map((achievement, i) => (
<View style={[styles.item, i !== 0 && styles.borderTop]} key={achievement.name}>
<Text style={styles.description}>{achievement.name}</Text>
{achievement.periods && achievement.periods.map((period) => {
let start = Moment(period.since);
start = start.isSame(Moment([1970, 1, 1]), 'day') ? '?' : start.format('D MMMM YYYY');
const end = period.until ? Moment(period.until).format('D MMMM YYYY') : 'heden';
let text = '';
if (period.role) {
text = `${period.role}: `;
} else if (period.chair) {
text = 'Voorzitter: ';
}
text += `${start} - ${end}`;
return <Text style={styles.data} key={period.since}>{text}</Text>;
})}
</View>
))}
</View>,
];
}
return <View />;
};
class Profile extends Component {
constructor(props) {
super(props);
this.scrollY = new Animated.Value(0);
}
render() {
const headerHeight = this.props.success ? this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
outputRange: [HEADER_MAX_HEIGHT, HEADER_MIN_HEIGHT],
extrapolate: 'clamp',
}) : HEADER_MIN_HEIGHT;
const imageOpacity = this.props.success ? this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
outputRange: [1, 1, 0],
extrapolate: 'clamp',
}) : 0;
const imageTranslate = this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE],
outputRange: [0, -50],
extrapolate: 'clamp',
});
const textSize = this.props.success ? this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
outputRange: [30, 30, 20],
extrapolate: 'clamp',
}) : 20;
const textPosLeft = this.props.success ? this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
outputRange: [20, 20, 60],
extrapolate: 'clamp',
}) : 60;
const textPosBottom = this.props.success ? this.scrollY.interpolate({
inputRange: [0, HEADER_SCROLL_DISTANCE / 2, HEADER_SCROLL_DISTANCE],
outputRange: [20, 20, (HEADER_MIN_HEIGHT - 24) / 2],
extrapolate: 'clamp',
}) : (HEADER_MIN_HEIGHT - 24) / 2;
return (
<View style={styles.container}>
{
this.props.success ? (
<ScrollView
style={styles.container}
scrollEventThrottle={16}
onScroll={Animated.event([{ nativeEvent: { contentOffset: { y: this.scrollY } } }])}
>
<View style={styles.content}>
{getDescription(this.props.profile)}
{getPersonalInfo(this.props.profile)}
{getAchievements(this.props.profile)}
</View>
</ScrollView>
) : (
<View style={styles.container}>
<Text style={styles.errorText}>
Ophalen profiel mislukt.
</Text>
</View>
)
}
<Animated.View style={[styles.header, { height: headerHeight }]}>
<Animated.Image
style={[
styles.backgroundImage,
{
opacity: imageOpacity,
transform: [{ translateY: imageTranslate }],
},
]}
source={{ uri: this.props.profile.photo }}
>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} />
</Animated.Image>
<Animated.View style={styles.appBar}>
<TouchableOpacity
onPress={this.props.back}
>
<Icon
name="arrow-back"
style={styles.icon}
size={24}
/>
</TouchableOpacity>
<Animated.Text
style={[styles.title, {
left: textPosLeft,
bottom: textPosBottom,
fontSize: textSize,
}]}
>{this.props.success ? this.props.profile.display_name : 'Profiel'}</Animated.Text>
</Animated.View>
</Animated.View>
</View>
);
}
}
Profile.propTypes = {
profile: PropTypes.shape({
pk: PropTypes.number.isRequired,
display_name: PropTypes.string.isRequired,
photo: PropTypes.string.isRequired,
profile_description: PropTypes.string,
birthday: PropTypes.string,
starting_year: PropTypes.number,
programme: PropTypes.string,
website: PropTypes.string,
membership_type: PropTypes.string,
achievements: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
earliest: PropTypes.string,
periods: PropTypes.arrayOf(PropTypes.shape({
chair: PropTypes.bool.isRequired,
until: PropTypes.string,
since: PropTypes.string.isRequired,
role: PropTypes.string,
})),
})).isRequired,
}).isRequired,
success: PropTypes.bool.isRequired,
back: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
profile: state.profile.profile,
success: state.profile.success,
});
const mapDispatchToProps = dispatch => ({
back: () => dispatch(back()),
});
export default connect(mapStateToProps, mapDispatchToProps)(Profile);
......@@ -7,8 +7,9 @@ import Icon from 'react-native-vector-icons/MaterialIcons';
import { colors } from '../style';
import styles from './style/sidebar';
import * as actions from '../actions/navigation';
import * as navigationActions from '../actions/navigation';
import * as loginActions from '../actions/login';
import { loadProfile } from '../actions/profile';
const background = require('../img/huygens.jpg');
......@@ -45,7 +46,7 @@ const Sidebar = (props) => {
style={styles.sidebar}
>
<TouchableHighlight
onPress={() => props.navigate('profile')}
onPress={() => props.loadProfile(props.token)}
style={styles.headerButton}
>
<Image
......@@ -88,19 +89,24 @@ Sidebar.propTypes = {
currentScene: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
photo: PropTypes.string.isRequired,
navigate: PropTypes.func.isRequired,
token: PropTypes.string.isRequired,
logout: PropTypes.func.isRequired,
loadProfile: PropTypes.func.isRequired,
// eslint-disable-next-line react/no-unused-prop-types
navigate: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
currentScene: state.navigation.currentScene,
displayName: state.session.displayName,
photo: state.session.photo,
token: state.session.token,
});
const mapDispatchToProps = dispatch => ({
navigate: (scene, newSection = false) => dispatch(actions.navigate(scene, newSection)),
navigate: (scene, newSection = false) => dispatch(navigationActions.navigate(scene, newSection)),
logout: () => dispatch(loginActions.logout()),
loadProfile: token => dispatch(loadProfile(token)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Sidebar);
......@@ -10,6 +10,7 @@ import Welcome from './Welcome';
import Sidebar from './Sidebar';
import Event from './Event';
import Calendar from './Calendar';
import Profile from './Profile';
import * as actions from '../actions/navigation';
import styles from './style/navigator';
......@@ -36,6 +37,8 @@ const sceneToComponent = (scene) => {
return <Event />;
case 'eventList':
return <Calendar />;
case 'profile':
return <Profile />;
default:
return <Welcome />;
}
......@@ -92,6 +95,7 @@ const ReduxNavigator = (props) => {
<View style={styles.statusBar}>
<StatusBar backgroundColor={colors.darkMagenta} barStyle="light-content" />
</View>
{currentScene !== 'profile' && (
<View style={styles.appBar}>
<TouchableOpacity
onPress={props.isFirstScene ? () => props.updateDrawer(!props.drawerOpen) : props.back}
......@@ -104,7 +108,7 @@ const ReduxNavigator = (props) => {
/>
</TouchableOpacity>
<Text style={styles.title}>{sceneToTitle(currentScene)}</Text>
</View>
</View>)}
{sceneToComponent(currentScene)}
<SnackBar visible={loginState === 'success'} textMessage={'Login success'} />
</Drawer>);
......
......@@ -3,7 +3,7 @@ import { Platform, StyleSheet } from 'react-native';
import { colors } from '../../style';
const STATUSBAR_HEIGHT = Platform.OS === 'ios' ? 20 : 0;
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
export const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
export const TOTAL_BAR_HEIGHT = APPBAR_HEIGHT + 20;
......
import { StyleSheet } from 'react-native';
import { colors } from '../../style';
import { APPBAR_HEIGHT } from './navigator';
export const HEADER_MIN_HEIGHT = APPBAR_HEIGHT;
export const HEADER_MAX_HEIGHT = 200;
export const HEADER_SCROLL_DISTANCE = HEADER_MAX_HEIGHT - HEADER_MIN_HEIGHT;
const styles = StyleSheet.create({
container: {
backgroundColor: colors.background,
flex: 1,
},
card: {
backgroundColor: colors.white,
borderRadius: 2,
elevation: 2,
marginLeft: 8,
marginRight: 8,
marginTop: 10,
marginBottom: 10,
},
item: {
padding: 16,
backgroundColor: colors.white,
},
borderTop: {
borderTopColor: colors.dividerGrey,
borderTopWidth: 1,
},
description: {
fontSize: 14,
lineHeight: 24,
color: colors.black,
fontFamily: 'sans-serif-medium',
},
data: {
fontSize: 14,
lineHeight: 24,
color: colors.gray,
fontFamily: 'sans-serif-medium',
},
sectionHeader: {
backgroundColor: colors.background,
fontFamily: 'sans-serif-medium',
fontSize: 14,
color: colors.textColour,
marginLeft: 18,
},
marginTop: {
marginTop: 10,
},
italics: {
fontStyle: 'italic',
},
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
backgroundColor: colors.magenta,
overflow: 'hidden',
elevation: 4,
},
appBar: {
backgroundColor: colors.transparent,
flex: 1,
justifyContent: 'flex-start',
alignItems: 'flex-start',
flexWrap: 'wrap',
flexDirection: 'row',
},
content: {
marginTop: HEADER_MAX_HEIGHT,
},
icon: {
fontSize: 24,
marginLeft: 16,
marginTop: (HEADER_MIN_HEIGHT - 24) / 2,
color: colors.white,
},
title: {
color: colors.white,
fontFamily: 'sans-serif-medium',
position: 'absolute',
},
backgroundImage: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
width: null,
height: HEADER_MAX_HEIGHT,
resizeMode: 'cover',
},
overlayGradient: {
position: 'absolute',
top: '50%',
bottom: 0,
left: 0,
right: 0,
opacity: 0.3,
},
errorText: {
marginTop: HEADER_MIN_HEIGHT,
},
});
export default styles;
......@@ -3,6 +3,7 @@ import navigation from './navigation';
import events from './events';
import calendar from './calendar';
import welcome from './welcome';
import profile from './profile';
export {
session,
......@@ -10,4 +11,5 @@ export {
events,
calendar,
welcome,
profile,
};
import * as types from '../actions/actionTypes';
const initialState = {
profile: {
pk: -1,
display_name: '',
photo: '',
profile_description: '',
birthday: '',
starting_year: -1,
programme: '',
website: '',
membership_type: '',
achievements: [],
},
success: false,
};
export default function profile(state = initialState, action = {}) {
switch (action.type) {
case types.LOADPROFILESUCCESS:
return {
...state,
profile: action.profile,
success: true,
};
case types.LOADPROFILEFAILURE:
return {
...state,
success: false,
};
default:
return state;
}
}