Commit 8577c76a authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Merge branch 'feature/pizzas' into 'master'

Add functionality to order pizza in the app.

Closes #12

See merge request !66
parents c15d95df d6063666
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 Icon from 'react-native-vector-icons/MaterialIcons';
import Moment from 'moment';
import 'moment/locale/nl';
import LoadingScreen from './LoadingScreen';
import { retrievePizzaInfo, cancelOrder, orderPizza } from '../actions/pizza';
import styles from './style/pizza';
import { colors } from '../style';
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;
};
getEventInfo = (title, subtitle) => (
<View style={styles.eventInfo}>
<Text style={styles.title}>Order pizza for {title}</Text>
<Text style={styles.subtitle}>{subtitle}</Text>
</View>
);
getOverview = (order, pizzaList) => {
if (order) {
const productInfo = this.getProductFromList(order.product, pizzaList);
return (
<View
style={[styles.overview, order.paid ? styles.greenBackground : styles.redBackground]}
>
<Text
style={styles.overviewText}
numberOfLines={3}
>{productInfo.name}</Text>
</View>
);
}
return <Text style={styles.header}>You did not place an order.</Text>;
};
getOrder = (order, pizzaList, hasEnded) => {
if (order) {
const productInfo = this.getProductFromList(order.product, pizzaList);
return (
<View style={styles.section}>
<Text style={styles.header}>Current order</Text>
<View style={styles.card}>
<Text
style={[styles.orderStatus, order.paid ? styles.paidStatus : styles.notPaidStatus]}
>The order has {order.paid || 'not yet '}been paid for.</Text>
<View style={styles.pizzaContainer}>
<View style={styles.pizzaInfo}>
<Text style={styles.pizzaName}>{productInfo.name}</Text>
<Text style={styles.pizzaDescription}>{productInfo.description}</Text>
<Text style={styles.pizzaPrice}>{productInfo.price}</Text>
</View>
{(!order.paid && !hasEnded) && (
<TouchableOpacity
onPress={() => this.props.cancelOrder(this.props.token)}
style={styles.button}
>
<Icon
name="delete"
color={colors.white}
size={18}
/>
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
return null;
};
getPizzaList = (pizzaList, hasOrder) => (
<View style={styles.section}>
{hasOrder && (
<Text style={styles.header}>Changing your order</Text>
)}
<View style={styles.card}>
{pizzaList.map(pizza => (
<View
key={pizza.pk}
style={styles.pizzaContainer}
>
<View style={styles.pizzaInfo}>
<Text style={styles.pizzaName}>{pizza.name}</Text>
<Text style={styles.pizzaDescription}>{pizza.description}</Text>
<Text style={styles.pizzaPrice}>{pizza.price}</Text>
</View>
<TouchableOpacity
onPress={() => this.props.orderPizza(this.props.token, pizza.pk, hasOrder)}
style={styles.button}
>
<Icon
name="edit"
color={colors.white}
size={18}
/>
</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 (
<ScrollView
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
}
>
<View style={styles.content}>
<Text
style={styles.title}
>Something went wrong while retrieving pizza info.</Text>
</View>
</ScrollView>
);
} else if (!this.props.event) {
return (
<ScrollView
refreshControl={
<RefreshControl
refreshing={this.state.refreshing}
onRefresh={this.handleRefresh}
/>
}
>
<View style={styles.content}>
<Text
style={styles.title}
>There is currently no event for which you can order food.</Text>
</View>
</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}>
{this.getEventInfo(this.props.event.title, subtitle)}
{hasEnded && this.getOverview(this.props.order, this.props.pizzaList)}
{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: 8,
},
eventInfo: {
padding: 8,
},
title: {
fontFamily: 'sans-serif-medium',
color: colors.textColour,
fontSize: 20,
marginBottom: 8,
},
subtitle: {
fontFamily: 'sans-serif-medium',
color: colors.textColour,
fontSize: 14,
},
overview: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
elevation: 2,
borderRadius: 2,
padding: 8,
height: 100,
},
overviewText: {
fontFamily: 'sans-serif-medium',
color: colors.white,
fontSize: 32,
},
greenBackground: {
backgroundColor: colors.lightGreen,
},
redBackground: {
backgroundColor: colors.lightRed,
},
section: {
marginTop: 8,
marginBottom: 8,
},
header: {
fontFamily: 'sans-serif-medium',
color: colors.textColour,
fontSize: 14,
paddingLeft: 10,
paddingTop: 6,
paddingBottom: 6,
},
card: {
backgroundColor: colors.white,
borderRadius: 2,
elevation: 2,
},
orderStatus: {
fontFamily: 'sans-serif-medium',
color: colors.white,
fontSize: 14,
padding: 16,
borderBottomWidth: 1,
borderTopRightRadius: 2,
borderTopLeftRadius: 2,
},
paidStatus: {
backgroundColor: colors.lightGreen,
borderBottomColor: colors.darkGreen,
},
notPaidStatus: {
backgroundColor: colors.lightRed,
borderBottomColor: colors.darkRed,
},
pizzaContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingLeft: 16,
paddingRight: 16,
paddingTop: 8,
paddingBottom: 8,
borderBottomWidth: 1,
borderBottomColor: colors.dividerGrey,
},
pizzaInfo: {
flex: 1,
},
pizzaName: {
fontFamily: 'sans-serif-medium',
color: colors.black,
fontSize: 14,
},
pizzaDescription: {
fontFamily: 'sans-serif-regular',
color: colors.gray,
fontSize: 14,
marginTop: 2,
marginBottom: 2,
},
pizzaPrice: {
fontFamily: 'sans-serif-medium',
color: colors.magenta,
fontSize: 14,
},
button: {
backgroundColor: colors.magenta,
padding: 8,
marginLeft: 16,
borderRadius: 2,
elevation: 2,
},
});
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;
}
}
......@@ -27,6 +27,7 @@ const event = function* event(action) {
};
const eventRegistrations = yield call(apiRequest, `events/${pk}/registrations`, data, params);
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),
]);
};
......
......@@ -30,9 +30,6 @@ const login = function* login(action) {
try {
let response = yield call(apiRequest, 'token-auth', data);
const { token } = response;
if (!token) {
throw Error();
}
data = {
method: 'GET',
headers: {
......
import { call, put, takeEvery } from 'redux-saga/effects';
import { apiRequest } from '../url';
import * as pizzaActions from '../actions/pizza';
import * as navigationActions from '../actions/navigation';
const NOT_FOUND = 404;
const retrievePizzaInfo = function* retrievePizzaInfo(action) {
const { token } = action.payload;
yield put(pizzaActions.fetching());
yield put(navigationActions.navigate('pizza'));