Commit b55da3b9 authored by Gijs Hendriksen's avatar Gijs Hendriksen
Browse files

Added member list component

parent 7b3d6404
export const MEMBERS = 'MEMBERS_MEMBERS';
export const FETCHING = 'MEMBERS_FETCHING';
export const MEMBERS_SUCCESS = 'MEMBERS_MEMBERS_SUCCESS';
export const FAILURE = 'MEMBERS_FAILURE';
export const MORE = 'MEMBERS_MORE';
export const MORE_SUCCESS = 'MEMBERS_MORE_SUCCESS';
export function members(keywords) {
return {
type: MEMBERS,
payload: { keywords },
};
}
export function fetching() {
return {
type: FETCHING,
};
}
export function success(memberList, next, searchKey) {
return {
type: MEMBERS_SUCCESS,
payload: { memberList, next, searchKey },
};
}
export function failure() {
return {
type: FAILURE,
};
}
export function more(url) {
return {
type: MORE,
payload: { url },
};
}
export function moreSuccess(memberList, next) {
return {
type: MORE_SUCCESS,
payload: { memberList, next },
};
}
import React, { Component } from 'react';
import { View, FlatList } from 'react-native';
import { connect } from 'react-redux';
import { translate } from 'react-i18next';
import PropTypes from 'prop-types';
import MemberView from './MemberView';
import LoadingScreen from './LoadingScreen';
import ErrorScreen from './ErrorScreen';
import SearchBar from './SearchHeader';
import * as memberActions from '../actions/members';
import styles, { memberSize } from './style/memberList';
class MemberList extends Component {
constructor(props) {
super(props);
this.state = {
refreshing: false,
};
}
componentDidMount = () => {
this.props.loadMembers(this.props.searchKey);
};
handleRefresh = () => {
this.props.loadMembers(this.props.searchKey);
};
handleEndReached = () => {
if (this.props.more !== null) {
this.props.loadMoreMembers(this.props.more);
}
};
render() {
const header = (
<SearchBar
title={this.props.t('Member List')}
searchText={this.props.t('Find a member')}
search={key => this.props.loadMembers(key)}
searchKey={this.props.searchKey}
/>
);
if (this.props.status === 'initial') {
return (
<View style={styles.wrapper}>
{header}
<LoadingScreen />
</View>
);
} else if (this.props.status === 'failure') {
return (
<View style={styles.wrapper}>
{header}
<ErrorScreen message={this.props.t('Sorry! We couldn\'t load any data.')} />
</View>
);
} else if (this.props.memberList.length === 0) {
return (
<View style={styles.wrapper}>
{header}
<ErrorScreen message={this.props.t('Couldn\'t find any members...')} />
</View>
);
}
return (
<View style={styles.wrapper}>
{header}
<View>
<FlatList
style={styles.flatList}
contentContainerStyle={styles.container}
onRefresh={this.handleRefresh}
refreshing={this.props.loading}
onEndReachedThreshold={0.5}
onEndReached={this.handleEndReached}
data={this.props.memberList}
renderItem={item => (
<MemberView
key={item.item.pk}
member={{
pk: item.item.pk,
photo: item.item.avatar.medium,
name: item.item.display_name,
}}
style={styles.memberView}
size={memberSize}
/>
)}
keyExtractor={item => item.pk}
numColumns={3}
/>
</View>
</View>
);
}
}
MemberList.defaultProps = {
more: null,
};
MemberList.propTypes = {
memberList: PropTypes.arrayOf(PropTypes.shape({
pk: PropTypes.number.isRequired,
display_name: PropTypes.string.isRequired,
photo: PropTypes.string.isRequired,
})).isRequired,
status: PropTypes.string.isRequired,
loading: PropTypes.bool.isRequired,
more: PropTypes.string,
searchKey: PropTypes.string.isRequired,
loadMembers: PropTypes.func.isRequired,
loadMoreMembers: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
memberList: state.members.memberList,
status: state.members.status,
loading: state.members.loading,
more: state.members.more,
searchKey: state.members.searchKey,
});
const mapDispatchToProps = dispatch => ({
loadMembers: (keywords = null) => dispatch(memberActions.members(keywords)),
loadMoreMembers: url => dispatch(memberActions.more(url)),
});
export default connect(mapStateToProps, mapDispatchToProps)(translate('memberList')(MemberList));
......@@ -24,9 +24,9 @@ const MemberView = props => (
MemberView.propTypes = {
member: PropTypes.shape({
pk: PropTypes.number,
name: PropTypes.string,
photo: PropTypes.string,
pk: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
photo: PropTypes.string.isRequired,
}).isRequired,
size: PropTypes.number.isRequired,
style: ViewPropTypes.style,
......
import React, { Component } from 'react';
import { StatusBar, Animated, Easing, BackHandler, TouchableOpacity, TextInput, Text, Platform, View } from 'react-native';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { updateDrawer } from '../actions/navigation';
import styles from './style/searchHeader';
import { colors } from '../style';
class SearchBar extends Component {
constructor(props) {
super(props);
this.state = {
isSearching: Boolean(props.searchKey),
isAnimating: Boolean(props.searchKey),
scaleValue: new Animated.Value(props.searchKey ? 1 : 0.01),
searchKey: props.searchKey,
};
BackHandler.addEventListener('hardwareBackPress', () => {
if (this.state.isSearching) {
this.updateSearch(false);
return true;
}
return false;
});
}
getLeftIcon = () => (
<TouchableOpacity
onPress={this.state.isSearching ? () => this.updateSearch(false) : this.props.openDrawer}
>
<Icon
name={this.state.isSearching ? 'arrow-back' : 'menu'}
style={[styles.leftIcon, this.state.isSearching ? styles.magenta : styles.white]}
size={24}
/>
</TouchableOpacity>
);
getCenter = () => (
this.state.isSearching ? (
<TextInput
style={styles.input}
selectionColor={colors.magenta}
placeholderTextColor={colors.lightGray}
underlineColorAndroid={colors.transparent}
placeholder={this.props.searchText}
onChangeText={this.updateSearchKey}
value={this.state.searchKey}
/>
) : <Text style={styles.title}>{this.props.title}</Text>
);
getRightIcon = () => {
if (!this.state.isSearching) {
return (
<TouchableOpacity
onPress={() => this.updateSearch(true)}
>
<Icon
name="search"
style={[styles.rightIcon, styles.white]}
size={24}
/>
</TouchableOpacity>
);
} else if (this.state.searchKey) {
return (
<TouchableOpacity
onPress={() => this.updateSearchKey('')}
>
<Icon
name="close"
style={[styles.rightIcon, styles.gray]}
size={24}
/>
</TouchableOpacity>
);
}
return null;
};
updateSearch = (isSearching) => {
this.setState({
isSearching,
});
if (isSearching) {
this.setState({
isAnimating: true,
});
Animated.timing(this.state.scaleValue, {
toValue: 1,
duration: 325,
easing: Easing.easeIn,
useNativeDriver: Platform.OS === 'android',
}).start();
} else {
this.updateSearchKey('');
Animated.timing(this.state.scaleValue, {
toValue: 0.01,
duration: 325,
easing: Easing.easeOut,
useNativeDriver: Platform.OS === 'android',
}).start(() => {
this.setState({
isAnimating: false,
});
});
}
};
updateSearchKey = (searchKey) => {
if (this.state.searchKey !== searchKey) {
this.setState({ searchKey });
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
this.props.search(searchKey);
}, 500);
}
};
render() {
return (
<View>
<StatusBar
backgroundColor={colors.semiTransparent}
translucent
animated
barStyle="light-content"
/>
<View style={styles.appBar}>
{this.state.isAnimating && <Animated.View
style={[
styles.animationView, { transform: [{ scale: this.state.scaleValue }] }]}
/>}
{this.getLeftIcon()}
{this.getCenter()}
{this.getRightIcon()}
</View>
</View>
);
}
}
SearchBar.propTypes = {
title: PropTypes.string.isRequired,
searchText: PropTypes.string.isRequired,
openDrawer: PropTypes.func.isRequired,
search: PropTypes.func.isRequired,
searchKey: PropTypes.string.isRequired,
};
const mapHeaderDispatchToProps = dispatch => ({
openDrawer: () => dispatch(updateDrawer(true)),
});
export default connect(() => ({}), mapHeaderDispatchToProps)(SearchBar);
......@@ -38,6 +38,13 @@ const Sidebar = (props) => {
style: {},
scene: 'eventList',
},
{
onPress: () => props.navigate('members', true),
iconName: 'people',
text: props.t('Member List'),
style: {},
scene: 'members',
},
{
onPress: logoutPrompt(props),
iconName: 'lock',
......
......@@ -13,6 +13,7 @@ import Profile from './Profile';
import Pizza from './Pizza';
import StandardHeader from './StandardHeader';
import Registration from './Registration';
import MemberList from './MemberList';
import * as actions from '../actions/navigation';
import styles from './style/navigator';
......@@ -32,6 +33,8 @@ const sceneToComponent = (scene) => {
return <Pizza />;
case 'registration':
return <Registration />;
case 'members':
return <MemberList />;
default:
return <Welcome />;
}
......@@ -72,7 +75,7 @@ const ReduxNavigator = (props) => {
onClose={() => updateDrawer(false)}
tapToClose
>
{currentScene !== 'profile' && <StandardHeader />}
{currentScene !== 'profile' && currentScene !== 'members' && <StandardHeader />}
{sceneToComponent(currentScene)}
</Drawer>);
}
......
import { Dimensions } from 'react-native';
import { StyleSheet, colors } from '../../style';
import { TOTAL_BAR_HEIGHT } from './standardHeader';
const windowWidth = Dimensions.get('window').width;
export const memberSize = (windowWidth - 64) / 3;
const styles = StyleSheet.create({
wrapper: {
flex: 1,
flexDirection: 'column',
backgroundColor: colors.background,
},
container: {
padding: 16,
},
flatList: {
backgroundColor: colors.background,
height: Dimensions.get('window').height - TOTAL_BAR_HEIGHT,
paddingBottom: 16,
},
memberView: {
marginRight: 16,
marginBottom: 16,
},
buttonList: {
flex: 1,
padding: 16,
flexDirection: 'row',
justifyContent: 'space-around',
alignItems: 'flex-end',
},
button: {
color: colors.magenta,
fontFamily: 'sans-serif-medium',
fontSize: 18,
},
disabled: {
color: colors.darkGrey,
},
});
export default styles;
import { Dimensions } from 'react-native';
import { StyleSheet, colors } from '../../style';
import { STATUSBAR_HEIGHT, APPBAR_HEIGHT } from './standardHeader';
const windowWidth = Dimensions.get('window').width;
const styles = StyleSheet.create({
appBar: {
backgroundColor: colors.magenta,
height: APPBAR_HEIGHT + STATUSBAR_HEIGHT,
paddingTop: STATUSBAR_HEIGHT,
justifyContent: 'flex-start',
alignItems: 'center',
flexWrap: 'wrap',
flexDirection: 'row',
elevation: 4,
},
title: {
color: colors.white,
fontSize: 20,
fontFamily: 'sans-serif-medium',
flex: 1,
},
leftIcon: {
paddingLeft: 20,
paddingRight: 32,
},
rightIcon: {
paddingLeft: 32,
paddingRight: 20,
},
white: {
color: colors.white,
},
magenta: {
color: colors.magenta,
},
gray: {
color: colors.lightGray,
},
input: {
color: colors.textColour,
fontSize: 20,
flex: 1,
paddingLeft: 0,
},
animationView: {
backgroundColor: colors.white,
width: windowWidth * 2,
height: windowWidth * 2,
borderRadius: windowWidth,
position: 'absolute',
right: 32 - windowWidth,
},
});
export default styles;
......@@ -4,6 +4,7 @@ const eventNL = require('./nl/event.json');
const eventCardNL = require('./nl/eventCard.json');
const eventDetailCardNL = require('./nl/eventDetailCard.json');
const loginNL = require('./nl/login.json');
const memberListNL = require('./nl/memberList.json');
const pizzaNL = require('./nl/pizza.json');
const profileNL = require('./nl/profile.json');
const registrationNL = require('./nl/registration.json');
......@@ -19,6 +20,7 @@ export default {
eventCard: eventCardNL,
eventDetailCard: eventDetailCardNL,
login: loginNL,
memberList: memberListNL,
pizza: pizzaNL,
profile: profileNL,
registration: registrationNL,
......
{
"Sorry! We couldn't load any data.": "Sorry, we konden geen gegevens laden.",
"Couldn't find any members...": "Er zijn geen leden gevonden...",
"Member List": "Ledenlijst",
"Find a member": "Zoek een lid"
}
......@@ -5,5 +5,6 @@
"Yes": "Ja",
"Welcome": "Welkom",
"Calendar": "Agenda",
"Logout": "Uitloggen"
"Logout": "Uitloggen",
"Member List": "Ledenlijst"
}
......@@ -6,6 +6,7 @@ import welcome from './welcome';
import profile from './profile';
import pizza from './pizza';
import registration from './registration';
import members from './members';
export {
session,
......@@ -16,4 +17,5 @@ export {
profile,
pizza,
registration,
members,
};
import * as memberActions from '../actions/members';
const initialState = {
memberList: [],
status: 'initial',
loading: false,
more: null,
searchKey: '',
};
export default function loadEvent(state = initialState, action = {}) {
switch (action.type) {
case memberActions.FETCHING: {
return {
...state,
loading: true,
};
}
case memberActions.MEMBERS_SUCCESS: {
return {
...state,
status: 'success',
loading: false,
memberList: action.payload.memberList,
more: action.payload.next,
searchKey: action.payload.searchKey,
};
}
case memberActions.FAILURE: {
return {
...state,
status: 'failure',
loading: false,
};
}
case memberActions.MORE_SUCCESS: {
return {
...state,
status: 'success',
loading: false,
memberList: [
...state.memberList,
...action.payload.memberList,
],
more: action.payload.next,
};
}
default:
return state;
}
}
......@@ -9,6 +9,7 @@ import pushNotificationsSaga from './pushNotifications';
import pizzaSaga from './pizza';
import registrationSaga from './registration';