Commit 518e6b6c authored by Gijs Hendriksen's avatar Gijs Hendriksen Committed by Sébastiaan Versteeg
Browse files

Add functionality to order pizza in the app.

parent c15d95df
export const PIZZA = 'PIZZA_PIZZALIST';
export const FETCHING = 'PIZZA_FETCHING';
export const SUCCESS = 'PIZZA_SUCCESS';
export const FAILURE = 'PIZZA_FAILURE';
export const CANCEL = 'PIZZA_CANCEL';
export const CANCEL_SUCCESS = 'PIZZA_CANCEL_SUCCESS';
export const ORDER = 'PIZZA_ORDER';
export const ORDER_SUCCESS = 'PIZZA_ORDER_SUCCESS';
export function retrievePizzaInfo(token) {
return {
type: PIZZA,
payload: { token },
};
}
export function success(event, order, pizzaList) {
return {
type: SUCCESS,
payload: { event, order, pizzaList },
};
}
export function fetching() {
return {
type: FETCHING,
};
}
export function failure() {
return {
type: FAILURE,
};
}
export function cancelOrder(token) {
return {
type: CANCEL,
payload: { token },
};
}
export function cancelSuccess() {
return {
type: CANCEL_SUCCESS,
};
}
export function orderPizza(token, pk, hasOrder) {
return {
type: ORDER,
payload: { token, pk, hasOrder },
};
}
export function orderSuccess(order) {
return {
type: ORDER_SUCCESS,
payload: { order },
};
}
import React from 'react';
import PropTypes from 'prop-types';
import { View, Text, TouchableOpacity, Linking } from 'react-native';
import { View, Text, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import Moment from 'moment';
import 'moment/locale/nl';
import * as actions from '../actions/event';
import { pizzaUrl } from '../url';
import { retrievePizzaInfo } from '../actions/pizza';
import styles from './style/eventDetailCard';
......@@ -48,7 +49,7 @@ const EventDetailCard = props => (
</TouchableOpacity>
{props.event.pizza ? (
<TouchableOpacity
onPress={() => Linking.openURL(pizzaUrl)}
onPress={() => props.retrievePizzaInfo(props.token)}
style={styles.button}
>
<Text style={styles.orderPizza}>PIZZA</Text>
......@@ -80,6 +81,7 @@ EventDetailCard.propTypes = {
}).isRequired,
loadEvent: PropTypes.func.isRequired,
token: PropTypes.string.isRequired,
retrievePizzaInfo: PropTypes.func.isRequired,
};
const mapStateToProps = state => ({
......@@ -88,6 +90,7 @@ const mapStateToProps = state => ({
const mapDispatchToProps = dispatch => ({
loadEvent: (pk, token) => dispatch(actions.event(pk, token)),
retrievePizzaInfo: token => dispatch(retrievePizzaInfo(token)),
});
export default connect(mapStateToProps, mapDispatchToProps)(EventDetailCard);
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { View, Text, ScrollView, RefreshControl, TouchableOpacity } from 'react-native';
import { connect } from 'react-redux';
import Moment from 'moment';
import 'moment/locale/nl';
import LoadingScreen from './LoadingScreen';
import { retrievePizzaInfo, cancelOrder, orderPizza } from '../actions/pizza';
import styles from './style/pizza';
class Pizza extends Component {
constructor(props) {
super(props);
this.state = {
refreshing: false,
};
}
getProductFromList = (pk, pizzaList) => {
for (let i = 0; i < pizzaList.length; i += 1) {
if (pizzaList[i].pk === pk) {
return pizzaList[i];
}
}
return null;
};
getOrder = (order, pizzaList, hasEnded) => {
if (order) {
const productInfo = this.getProductFromList(order.product, pizzaList);
if (hasEnded) {
return (
<View>
<View
style={[
styles.currentOrder,
order.paid ? styles.greenBackground : styles.redBackground,
]}
>
<Text
style={order.paid ? styles.paidStatus : styles.notPaidStatus}
>The order has {order.paid || 'not yet '}been paid for.</Text>
<Text
style={[
styles.finalOrder,
order.paid ? styles.greenText : styles.whiteText,
]}
>{productInfo.name}</Text>
</View>
<Text style={styles.subtitle}>You can no longer cancel.</Text>
</View>
);
}
return (
<View style={styles.currentOrder}>
<Text
style={order.paid ? styles.paidStatus : styles.notPaidStatus}
>The order has {order.paid || 'not yet '}been paid for.</Text>
<View
style={styles.currentOrderInfo}
>
<View>
<Text style={styles.name}>{productInfo.name}</Text>
<Text style={styles.description}>{productInfo.description}</Text>
<Text style={styles.price}>{productInfo.price}</Text>
</View>
<View>
<TouchableOpacity
onPress={() => this.props.cancelOrder(this.props.token)}
style={[styles.button, order.paid && styles.disabled]}
disabled={order.paid}
>
<Text style={styles.buttonText}>CANCEL</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
} else if (hasEnded) {
return <Text>You did not place an order.</Text>;
}
return null;
};
getPizzaList = (pizzaList, hasOrder) => [(
hasOrder && <Text
key="description"
style={styles.title}
>Changing your order</Text>
), (
<View key="content" style={styles.pizzaCard}>
{pizzaList.map(pizza => (
<View
key={pizza.pk}
style={styles.pizzaDetail}
>
<View>
<Text style={styles.name}>{pizza.name}</Text>
<Text style={styles.description}>{pizza.description}</Text>
<Text style={styles.price}>{pizza.price}</Text>
</View>
<View>
<TouchableOpacity
onPress={() => this.props.orderPizza(this.props.token, pizza.pk, hasOrder)}
style={styles.button}
>
<Text style={styles.buttonText}>{hasOrder ? 'MODIFY' : 'ORDER'}</Text>
</TouchableOpacity>
</View>
</View>
))}
</View>
)];
handleRefresh = () => {
this.setState({ refreshing: true });
this.props.retrievePizzaInfo(this.props.token);
this.setState({ refreshing: false });
};
render() {
if (!this.props.hasLoaded) {
return <LoadingScreen />;
} else if (!this.props.success) {
return <Text>Something went wrong.</Text>;
}
if (!this.props.event) {
return (
<ScrollView
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
}
>
<Text
style={styles.title}
>There is currently no event for which you can order food.</Text>
</ScrollView>
);
}
const start = Moment(this.props.event.start);
const end = Moment(this.props.event.end);
const now = Moment();
const inFuture = start.diff(now, 'm') > 0;
const hasEnded = end.diff(now, 'm') < 0;
let subtitle;
if (inFuture) {
subtitle = `It will be possible to order from ${start.format('HH:mm')}`;
} else if (hasEnded) {
subtitle = `It was possible to order until ${end.format('HH:mm')}`;
} else {
subtitle = `You can order until ${end.format('HH:mm')}`;
}
return (
<ScrollView
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
}
>
<View style={styles.content}>
<Text style={styles.title}>Order pizza for {this.props.event.title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
{this.getOrder(this.props.order, this.props.pizzaList, hasEnded)}
{inFuture || hasEnded || (this.props.order && this.props.order.paid) ||
this.getPizzaList(this.props.pizzaList, !!this.props.order)}
</View>
</ScrollView>
);
}
}
Pizza.propTypes = {
success: PropTypes.bool.isRequired,
hasLoaded: PropTypes.bool.isRequired,
event: PropTypes.shape({
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired,
event: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
}),
order: PropTypes.shape({
pk: PropTypes.number.isRequired,
paid: PropTypes.bool.isRequired,
product: PropTypes.number.isRequired,
name: PropTypes.string,
member: PropTypes.number,
}),
pizzaList: PropTypes.arrayOf(PropTypes.shape({
pk: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
price: PropTypes.string.isRequired,
available: PropTypes.bool.isRequired,
})).isRequired,
token: PropTypes.string.isRequired,
retrievePizzaInfo: PropTypes.func.isRequired,
cancelOrder: PropTypes.func.isRequired,
orderPizza: PropTypes.func.isRequired,
};
Pizza.defaultProps = {
event: null,
order: null,
};
const mapStateToProps = state => ({
success: state.pizza.success,
hasLoaded: state.pizza.success,
event: state.pizza.event,
order: state.pizza.order,
pizzaList: state.pizza.pizzaList,
token: state.session.token,
});
const mapDispatchToProps = dispatch => ({
retrievePizzaInfo: token => dispatch(retrievePizzaInfo(token)),
cancelOrder: token => dispatch(cancelOrder(token)),
orderPizza: (token, pk, hasOrder) => dispatch(orderPizza(token, pk, hasOrder)),
});
export default connect(mapStateToProps, mapDispatchToProps)(Pizza);
......@@ -11,6 +11,7 @@ import Sidebar from './Sidebar';
import Event from './Event';
import Calendar from './Calendar';
import Profile from './Profile';
import Pizza from './Pizza';
import * as actions from '../actions/navigation';
import styles from './style/navigator';
......@@ -39,6 +40,8 @@ const sceneToComponent = (scene) => {
return <Calendar />;
case 'profile':
return <Profile />;
case 'pizza':
return <Pizza />;
default:
return <Welcome />;
}
......@@ -52,6 +55,8 @@ const sceneToTitle = (scene) => {
return 'Evenement';
case 'eventList':
return 'Agenda';
case 'pizza':
return 'Pizza';
default:
return 'ThaliApp';
}
......
import { StyleSheet } from 'react-native';
import { colors } from '../../style';
const styles = StyleSheet.create({
content: {
flex: 1,
padding: 16,
},
title: {
color: colors.textColour,
fontFamily: 'sans-serif-medium',
fontSize: 20,
},
subtitle: {
color: colors.textColour,
fontFamily: 'sans-serif-medium',
fontSize: 16,
fontStyle: 'italic',
},
name: {
color: colors.textColour,
fontFamily: 'sans-serif-medium',
fontSize: 16,
},
price: {
color: colors.textColour,
fontFamily: 'sans-serif-medium',
},
description: {
color: colors.textColour,
fontFamily: 'sans-serif-medium',
fontStyle: 'italic',
},
button: {
backgroundColor: colors.magenta,
borderRadius: 2,
elevation: 2,
},
buttonText: {
fontFamily: 'sans-serif-medium',
color: colors.white,
padding: 10,
},
pizzaCard: {
elevation: 2,
borderRadius: 2,
},
pizzaDetail: {
borderBottomColor: colors.lightGray,
borderBottomWidth: 1,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: 16,
},
currentOrder: {
elevation: 2,
borderRadius: 2,
padding: 16,
marginTop: 10,
marginBottom: 10,
},
currentOrderInfo: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
paidStatus: {
fontFamily: 'sans-serif-medium',
backgroundColor: colors.lightGreen,
borderWidth: 1,
borderColor: colors.darkGreen,
color: colors.darkGreen,
flex: 1,
flexDirection: 'row',
padding: 10,
fontSize: 16,
marginBottom: 10,
},
notPaidStatus: {
fontFamily: 'sans-serif-medium',
backgroundColor: colors.red,
borderWidth: 1,
borderColor: colors.white,
color: colors.white,
flex: 1,
flexDirection: 'row',
padding: 10,
fontSize: 16,
marginBottom: 10,
},
finalOrder: {
flex: 1,
flexDirection: 'row',
textAlign: 'center',
fontFamily: 'sans-serif-medium',
color: colors.textColour,
fontSize: 40,
},
redBackground: {
backgroundColor: colors.red,
},
greenBackground: {
backgroundColor: colors.lightGreen,
},
whiteText: {
color: colors.white,
},
greenText: {
color: colors.darkGreen,
},
disabled: {
backgroundColor: colors.gray,
},
});
export default styles;
......@@ -4,6 +4,7 @@ import event from './event';
import calendar from './calendar';
import welcome from './welcome';
import profile from './profile';
import pizza from './pizza';
export {
session,
......@@ -12,4 +13,5 @@ export {
calendar,
welcome,
profile,
pizza,
};
import * as pizzaActions from '../actions/pizza';
const initialState = {
success: false,
hasLoaded: false,
event: null,
order: null,
pizzaList: [],
};
export default function pizza(state = initialState, action = {}) {
switch (action.type) {
case pizzaActions.SUCCESS:
return {
success: true,
hasLoaded: true,
event: action.payload.event,
order: action.payload.order,
pizzaList: action.payload.pizzaList,
};
case pizzaActions.FAILURE:
return {
...state,
success: false,
hasLoaded: true,
};
case pizzaActions.FETCHING:
return {
...state,
hasLoaded: false,
};
case pizzaActions.CANCEL_SUCCESS:
return {
...state,
order: null,
};
case pizzaActions.ORDER_SUCCESS:
return {
...state,
order: action.payload.order,
};
default:
return state;
}
}
......@@ -20,13 +20,24 @@ const event = function* event(action) {
};
try {
const eventData = yield call(apiRequest, `events/${pk}`, data);
let response = yield call(apiRequest, `events/${pk}`, data);
const eventData = response.content;
if (!response.success) {
throw Error();
}
const params = {
status: 'registered',
};
const eventRegistrations = yield call(apiRequest, `events/${pk}/registrations`, data, params);
response = yield call(apiRequest, `events/${pk}/registrations`, data, params);
const eventRegistrations = response.content;
if (!response.success) {
throw Error();
}
yield put(eventActions.success(
eventData,
eventRegistrations,
......
......@@ -6,6 +6,7 @@ import profileSaga from './profile';
import welcomeSaga from './welcome';
import calendarSaga from './calendar';
import pushNotificationsSaga from './pushNotifications';
import pizzaSaga from './pizza';
const sagas = function* sagas() {
yield all([
......@@ -15,6 +16,7 @@ const sagas = function* sagas() {
fork(welcomeSaga),
fork(calendarSaga),
fork(pushNotificationsSaga),
fork(pizzaSaga),
]);
};
......
......@@ -29,8 +29,8 @@ const login = function* login(action) {
};
try {
let response = yield call(apiRequest, 'token-auth', data);
const { token } = response;
if (!token) {
const { token } = response.content;
if (!response.success) {
throw Error();
}
data = {
......@@ -42,8 +42,11 @@ const login = function* login(action) {
},
};
response = yield call(apiRequest, 'members/me', data);
const displayName = response.display_name;
const avatar = response.photo === null ? defaultAvatar : response.photo;
if (!response.success) {
throw Error();
}
const displayName = response.content.display_name;
const avatar = response.content.photo === null ? defaultAvatar : response.content.photo;
yield call(AsyncStorage.multiSet, [
[USERNAMEKEY, user],
[TOKENKEY, token],
......
import { call, takeEvery, put } from 'redux-saga/effects';
import { apiRequest } from '../url';