Commit 5db69856 authored by Wietse Kuipers's avatar Wietse Kuipers
Browse files

Merge branch 'feature/event-details' into 'master'

Event details styling

See merge request !22
parents a9919c64 8865f022
...@@ -2,10 +2,11 @@ import * as types from './actionTypes'; ...@@ -2,10 +2,11 @@ import * as types from './actionTypes';
import { navigate } from './navigation'; import { navigate } from './navigation';
import { url } from '../url'; import { url } from '../url';
export function success(data) { export function success(data, registrations) {
return { return {
type: types.LOADEVENTSUCCESS, type: types.LOADEVENTSUCCESS,
data, data,
registrations,
}; };
} }
...@@ -15,6 +16,25 @@ export function fail() { ...@@ -15,6 +16,25 @@ export function fail() {
}; };
} }
function loadRegistrations(id, token) {
const data = {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Token ${token}`,
},
};
return fetch(`${url}/api/events/${id}/registrations/?status=registered`, data)
.then(
response => response.json(),
)
.catch(
() => [],
);
}
export function loadEvent(id, token) { export function loadEvent(id, token) {
return (dispatch) => { return (dispatch) => {
const data = { const data = {
...@@ -31,8 +51,16 @@ export function loadEvent(id, token) { ...@@ -31,8 +51,16 @@ export function loadEvent(id, token) {
) )
.then( .then(
(response) => { (response) => {
dispatch(success(response)); if (response.status > -1) {
dispatch(navigate('event')); loadRegistrations(id, token)
.then((registrations) => {
dispatch(success(response, registrations));
dispatch(navigate('event'));
});
} else {
dispatch(success(response, []));
dispatch(navigate('event'));
}
}, },
) )
.catch( .catch(
......
import React, { Component } from 'react'; import React, { Component } from 'react';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { createStore, applyMiddleware, combineReducers } from 'redux'; import { applyMiddleware, combineReducers, createStore } from 'redux';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import Moment from 'moment';
import 'moment/locale/nl';
import * as reducers from './reducers'; import * as reducers from './reducers';
import ReduxNavigator from './components/navigator'; import ReduxNavigator from './components/navigator';
...@@ -25,6 +27,7 @@ const pairsToObject = (obj, pair) => { ...@@ -25,6 +27,7 @@ const pairsToObject = (obj, pair) => {
class Main extends Component { class Main extends Component {
componentDidMount() { componentDidMount() {
Moment.locale('nl');
AsyncStorage.multiGet([USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY]) AsyncStorage.multiGet([USERNAMEKEY, TOKENKEY, DISPLAYNAMEKEY, PHOTOKEY])
.then( .then(
(result) => { (result) => {
......
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { Image, ScrollView, Text, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Moment from 'moment';
import 'moment/locale/nl';
import styles from './style/event'; import styles from './style/event';
import MemberView from './MemberView';
import { colors } from '../style';
const REGISTRATION_NOT_NEEDED = -1;
const REGISTRATION_NOT_YET_OPEN = 0;
// eslint-disable-next-line no-unused-vars
const REGISTRATION_OPEN = 1;
const REGISTRATION_OPEN_NO_CANCEL = 2;
const REGISTRATION_CLOSED = 3;
const REGISTRATION_CLOSED_CANCEL_ONLY = 4;
const Event = (props) => { const Event = (props) => {
if (props.success) { if (props.success) {
const eventDesc = (data) => {
const startDate = Moment(data.start).format('D MMM YYYY, HH:mm');
const endDate = Moment(data.end).format('D MMM YYYY, HH:mm');
const infoTexts = [];
// let text = '';
infoTexts.push(
<View key="start-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="start-title">Van:</Text>
<Text style={styles.infoValueText} key="start-value">{startDate}</Text>
</View>,
);
infoTexts.push(
<View key="end-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="end-title">Tot:</Text>
<Text style={styles.infoValueText} key="end-value">{endDate}</Text>
</View>,
);
infoTexts.push(
<View key="loc-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="loc-title">Locatie:</Text>
<Text style={styles.infoValueText} key="loc-value">{data.location}</Text>
</View>,
);
infoTexts.push(
<View key="price-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="price-title">Prijs:</Text>
<Text style={styles.infoValueText} key="price-value">{data.price}</Text>
</View>,
);
if (data.status > REGISTRATION_NOT_NEEDED) {
const registrationDeadline = Moment(data.registration_end).format('D MMM YYYY, HH:m');
const cancelDeadline = Moment(data.cancel_deadline).format('D MMM YYYY, HH:m');
infoTexts.push(
<View key="registrationend-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="registrationend-title">Aanmelddeadline:</Text>
<Text style={styles.infoValueText} key="registrationend-value">{registrationDeadline}</Text>
</View>,
);
infoTexts.push(
<View key="canceldeadline-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="canceldeadline-title">Afmelddeadline:</Text>
<Text style={styles.infoValueText} key="canceldeadline-value">{cancelDeadline}</Text>
</View>,
);
let participantsText = `${data.num_participants} aanmeldingen`;
if (data.max_participants) {
participantsText += ` (${data.max_participants} max)`;
}
infoTexts.push(
<View key="participants-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="participants-title">Aantal aanmeldingen:</Text>
<Text style={styles.infoValueText} key="participants-value">{participantsText}</Text>
</View>,
);
if (data.user_registration) {
let registrationState;
if (data.user_registration.is_late_cancellation) {
registrationState = 'Je bent afgemeld na de afmelddeadline';
} else if (data.user_registration.queue_position === null) {
registrationState = 'Je bent aangemeld';
} else if (data.user_registration.queue_position > 0) {
registrationState = `Wachtlijst positie ${data.user_registration.queue_position}`;
} else {
registrationState = 'Je bent afgemeld';
}
infoTexts.push(
<View key="status-holder" style={styles.infoHolder}>
<Text style={styles.infoText} key="status-title">Aanmeldstatus:</Text>
<Text style={styles.infoValueText} key="status-value">{registrationState}</Text>
</View>,
);
}
}
return (
<View>
{infoTexts}
</View>
);
};
const eventInfo = (event) => {
let text = '';
if (event.status === REGISTRATION_NOT_YET_OPEN) {
const registrationStart = Moment(event.registration_start).format('D MMM YYYY, HH:m');
text = `Aanmelden opent ${registrationStart}`;
} else if (event.status === REGISTRATION_CLOSED ||
event.status === REGISTRATION_CLOSED_CANCEL_ONLY) {
text = 'Aanmelden is niet meer mogelijk';
} else if (event.status === REGISTRATION_NOT_NEEDED) {
text = 'Geen aanmelding vereist';
if (event.no_registration_message) {
text = event.no_registration_message;
}
}
if ((event.status === REGISTRATION_OPEN_NO_CANCEL || event.status === REGISTRATION_CLOSED) &&
event.user_registration !== null &&
!event.user_registration.is_cancelled && event.fine > 0 &&
event.user_registration.queue_position === null) {
text += `Afmelden is niet meer mogelijk zonder de volledige kosten van €${event.fine} te ` +
'betalen. Let op: je kunt je hierna niet meer aanmelden.';
}
if (text.length > 0) {
return (<Text style={styles.registrationText}>{text}</Text>);
}
return (<View />);
};
// eslint-disable-next-line arrow-body-style
const eventActions = () => {
// Needed once registration on server implemented
// if (event.registration_allowed) {
// if ((event.user_registration === null || event.user_registration.is_cancelled) &&
// (event.status === REGISTRATION_OPEN || event.status === REGISTRATION_OPEN_NO_CANCEL)) {
// const text = event.max_participants < event.num_participants ?
// 'Aanmelden' : 'Zet me op de wachtlijst';
// return (
// <View style={styles.registrationActions}>
// <Button color={colors.magenta} title={text} onPress={() => {}} />
// </View>
// );
// } else if (event.user_registration && !event.user_registration.is_cancelled &&
// event.status !== REGISTRATION_NOT_NEEDED && event.status !== REGISTRATION_NOT_YET_OPEN) {
// if ((event.status === REGISTRATION_OPEN || event.status === REGISTRATION_OPEN_NO_CANCEL)
// && event.user_registration && !event.user_registration.is_cancelled
// && event.has_fields) {
// return (
// <View style={styles.registrationActions}>
// <Button
// color={colors.magenta} title="Aanmelding bijwerken" onPress={() => {}}
// />
// <View style={styles.secondButtonMargin}>
// <Button color={colors.magenta} title="Afmelden" onPress={() => {}} />
// </View>
// </View>
// );
// }
// return (
// <View style={styles.registrationActions}>
// <Button color={colors.magenta} title="Afmelden" onPress={() => {}} />
// </View>
// );
// }
// }
return (<View />);
};
const registrationsGrid = (registrations) => {
if (registrations !== undefined && registrations.length > 0) {
return (
<View>
<View style={styles.divider} />
<Text style={styles.registrationsTitle}>Aanmeldingen</Text>
<View style={styles.registrationsView}>
{/*
Create a grid for the registrations:
First create chunks of max 3 and map those to a View
Then inside that View create 3 MemberViews (if is a real registration) or
a placeholder View (to make sure flex space is filled)
*/}
{ props.registrations.map((item, index) => {
if (index % 3 === 0) {
return props.registrations.slice(index, index + 3);
}
return null;
}).filter(item => item)
.map((list) => {
while (list.length < 3) {
list.push({ pk: null });
}
const key = list[0].pk.toString().concat(list[1].pk, list[2].pk);
return (
<View key={key} style={styles.registrationsRow}>
{list.map((reg, i) => {
const style = i === 1 ? styles.registrationsItemMargin :
styles.registrationsItem;
if (reg.name) {
return (
<MemberView key={key + i.toString()} member={reg} style={style} />
);
}
return (
<View key={key + i.toString()} style={style} />
);
})}
</View>
);
})
}
</View>
</View>
);
}
return (<View />);
};
return ( return (
<View> <ScrollView backgroundColor={colors.background} contentContainerStyle={styles.eventView}>
<Image style={styles.locationImage} source={{ uri: `https://maps.googleapis.com/maps/api/staticmap?center=${props.data.map_location}&zoom=13&size=450x250&markers=${props.data.map_location}` }} />
<Text style={styles.titleText}>{props.data.title}</Text> <Text style={styles.titleText}>{props.data.title}</Text>
<Text>{props.data.description}</Text> {eventDesc(props.data)}
<Text>Locatie: {props.data.location}</Text> {eventActions(props.data)}
<Text>Prijs: {props.data.price}</Text> {eventInfo(props.data)}
<Text>Boete: {props.data.fine}</Text> <View style={styles.divider} />
</View> <Text style={styles.descText}>{props.data.description}</Text>
{registrationsGrid(props.registrations)}
</ScrollView>
); );
} }
return ( return (
<Text>Kon het evenement niet laden...</Text> <ScrollView backgroundColor={colors.background} contentContainerStyle={styles.eventView}>
<Text>Kon het evenement niet laden...</Text>
</ScrollView>
); );
}; };
Event.propTypes = { Event.propTypes = {
data: React.PropTypes.shape({ data: React.PropTypes.shape({
pk: React.PropTypes.number.isRequired,
title: React.PropTypes.string.isRequired, title: React.PropTypes.string.isRequired,
description: React.PropTypes.string.isRequired, description: React.PropTypes.string.isRequired,
start: React.PropTypes.string.isRequired, start: React.PropTypes.string.isRequired,
end: React.PropTypes.string.isRequired, end: React.PropTypes.string.isRequired,
organiser: React.PropTypes.oneOfType([ organiser: React.PropTypes.number.isRequired,
React.PropTypes.number,
React.PropTypes.string,
]).isRequired,
location: React.PropTypes.string.isRequired, location: React.PropTypes.string.isRequired,
price: React.PropTypes.string.isRequired, map_location: React.PropTypes.string.isRequired,
fine: React.PropTypes.string.isRequired, status: React.PropTypes.number.isRequired,
registration_allowed: React.PropTypes.bool.isRequired,
has_fields: React.PropTypes.bool.isRequired,
registration_start: React.PropTypes.string,
registration_end: React.PropTypes.string,
cancel_deadline: React.PropTypes.string,
max_participants: React.PropTypes.number,
num_participants: React.PropTypes.number,
price: React.PropTypes.string,
fine: React.PropTypes.string,
user_registration: React.PropTypes.shape({
registered_on: React.PropTypes.string,
queue_position: React.PropTypes.number,
is_cancelled: React.PropTypes.bool,
is_late_cancellation: React.PropTypes.bool,
}),
no_registration_message: React.PropTypes.string,
}).isRequired, }).isRequired,
registrations: React.PropTypes.arrayOf(React.PropTypes.shape({
pk: React.PropTypes.number.isRequired,
member: React.PropTypes.number,
name: React.PropTypes.string.isRequired,
})).isRequired,
success: React.PropTypes.bool.isRequired, success: React.PropTypes.bool.isRequired,
}; };
const mapStateToProps = state => ({ const mapStateToProps = state => ({
data: state.events.data, data: state.events.data,
registrations: state.events.registrations,
success: state.events.success, success: state.events.success,
}); });
......
import React from 'react';
import { Image, Text, ViewPropTypes, StyleSheet } from 'react-native';
import LinearGradient from 'react-native-linear-gradient';
import { connect } from 'react-redux';
import styles from './style/memberView';
import SquareView from './SquareView';
const MemberView = props => (
<SquareView style={props.style}>
<Image style={styles.image} source={{ uri: props.member.photo }}>
<LinearGradient colors={['#55000000', '#000000']} style={styles.overlayGradient} />
<Text style={styles.nameText}>{props.member.name}</Text>
</Image>
</SquareView>
);
MemberView.propTypes = {
member: React.PropTypes.shape({
name: React.PropTypes.string,
photo: React.PropTypes.string,
}).isRequired,
style: ViewPropTypes.style,
};
const defaultStyles = StyleSheet.create({
});
MemberView.defaultProps = {
style: defaultStyles,
};
export default connect(() => ({}), () => ({}))(MemberView);
import React, { Component } from 'react';
import { View, ViewPropTypes, StyleSheet } from 'react-native';
class SquareView extends Component {
constructor(props) {
super(props);
this.state = {
width: 0,
height: 0,
direction: 'column', // 'column' and 'row'
};
}
render() {
const square = (
<View
{...this.props}
style={[this.props.style, { width: this.state.width, height: this.state.height }]}
onLayout={(event) => {
const { width, height } = event.nativeEvent.layout;
const sideLength = Math.max(width, height);
if (sideLength) {
this.setState({ width: sideLength, height: sideLength });
} else {
this.setState({ direction: 'column' });
}
}}
>
{this.props.children}
</View>
);
switch (this.state.direction) {
case 'column':
return square;
case 'row':
// eslint-disable-next-line react-native/no-inline-styles,react-native/no-color-literals
return (<View style={{ backgroundColor: 'transparent' }}>{square}</View>);
default:
return null;
}
}
}
SquareView.propTypes = {
children: React.PropTypes.node.isRequired,
style: ViewPropTypes.style,
};
const styles = StyleSheet.create({
});
SquareView.defaultProps = {
style: styles,
};
export default SquareView;
import { StyleSheet } from 'react-native'; import { create, colors } from '../../style';
import { colors } from '../../style'; const styles = create({
eventView: {
const styles = StyleSheet.create({ padding: 16,
backgroundColor: colors.background,
},
locationImage: {
height: 150,
marginLeft: -16,
marginRight: -16,
marginTop: -16,
marginBottom: 24,
},
titleText: { titleText: {
fontSize: 24, marginBottom: 16,
fontWeight: 'bold', android: {
color: colors.darkMagenta, fontFamily: 'sans-serif-medium',
},
ios: {
fontFamily: 'System',
fontWeight: '500',
},
fontSize: 20,
lineHeight: 20.0,
color: colors.darkGrey,
},
infoHolder: {
flexDirection: 'row',
},
infoText: {
flex: 1,
android: {
fontFamily: 'sans-serif-medium',
},
ios: {
fontFamily: 'System',
fontWeight: '500',
},
fontSize: 14,
lineHeight: 22.0,
color: colors.darkGrey,
width: '50%',
paddingRight: 8,
},
infoValueText: {
flex: 1,
android: {
fontFamily: 'sans-serif',
},
ios: {
fontFamily: 'System',
fontWeight: '300',
},
fontSize: 14,
lineHeight: 22.0,
color: colors.darkGrey,
width: '50%',
},
registrationText: {
marginTop: 16,
},
registrationActions: {
flexDirection: 'row',
alignItems: 'flex-start',
marginTop: 16,
},
secondButtonMargin: {
marginLeft: 16,
},
descText: {
fontSize: 14,
lineHeight: 24.0,