Alerts, Animations, Gestures, and More!
In week 3 we will:
- Reveal buttons in response to a swipe gesture.
- Use the Alert API to show a dialog with options
- Use the Animated library, a flexible and powerful way to create animations built into React Native
- Also use a third party library called React Native Animatable to create basic animations quickly
- Use the built-in PanResponder API to create custom reactions to gestures
- Persist the Redux store through reloading of the application
Favorites
Notes about this step:
- Access campsites data from Redux state
- Use connect() function to connect to Redux store
- Use mapStateToProps function to pass campsites state to component as props
- No need for mapDispatchToProps object at this time
- Create a StackNavigator to use in the Drawer Navigator
- Set Stack Navigator header title using static navigationOptions
- Route to corresponding CampsiteInfo component using navigate function from the navigation prop
- Set up like Directory component, but with a filtered array of only favorited campsites instead of all campsites
- Add a new file named FavoritesComponent.js in the components folder and update its contents as follows:
import React, { Component } from 'react';
import { FlatList, View, Text } from 'react-native';
import { ListItem } from 'react-native-elements';
import { connect } from 'react-redux';
import { Loading } from './LoadingComponent';
import { baseUrl } from '../shared/baseUrl';
const mapStateToProps = state => {
return {
campsites: state.campsites,
favorites: state.favorites
};
};
class Favorites extends Component {
static navigationOptions = {
title: 'My Favorites'
}
render() {
const { navigate } = this.props.navigation;
const renderFavoriteItem = ({item}) => {
return (
<ListItem
title={item.name}
subtitle={item.description}
leftAvatar={{source: {uri: baseUrl + item.image}}}
onPress={() => navigate('CampsiteInfo', {campsiteId: item.id})}
/>
);
};
if (this.props.campsites.isLoading) {
return <Loading />;
}
if (this.props.campsites.errMess) {
return (
<View>
<Text>{this.props.campsites.errMess}</Text>
</View>
);
}
return (
<FlatList
data={this.props.campsites.campsites.filter(
campsite => this.props.favorites.includes(campsite.id)
)}
renderItem={renderFavoriteItem}
keyExtractor={item => item.id.toString()}
/>
);
}
}
export default connect(mapStateToProps)(Favorites);
MainComponent.js
import React, { Component } from ‘react’;
import Home from ‘./HomeComponent’;
import Directory from ‘./DirectoryComponent’;
import CampsiteInfo from ‘./CampsiteInfoComponent’;
import About from ‘./AboutComponent’;
import Contact from ‘./ContactComponent’;
import Reservation from ‘./ReservationComponent’;
import Favorites from ‘./FavoritesComponent’;
import { View, Platform, StyleSheet, Text, ScrollView, Image } from ‘react-native’;
import { createStackNavigator } from ‘react-navigation-stack’;
import { createDrawerNavigator, DrawerItems } from ‘react-navigation-drawer’;
import { createAppContainer } from ‘react-navigation’;
import { Icon } from ‘react-native-elements’;
import SafeAreaView from ‘react-native-safe-area-view’;
import { connect } from ‘react-redux’;
import { fetchCampsites, fetchComments, fetchPromotions, fetchPartners } from ‘../redux/ActionCreators’;
const mapDispatchToProps = {
fetchCampsites,
fetchComments,
fetchPromotions,
fetchPartners
};
const DirectoryNavigator = createStackNavigator(
{
Directory: {
screen: Directory,
navigationOptions: ({navigation}) => ({
headerLeft: <Icon
name=’list’
type=’font-awesome’
iconStyle={styles.stackIcon}
onPress={() => navigation.toggleDrawer()}
/>
})
},
CampsiteInfo: { screen: CampsiteInfo }
},
{
initialRouteName: ‘Directory’,
defaultNavigationOptions: {
headerStyle: {
backgroundColor: ‘#5637DD’
},
headerTintColor: ‘#fff’,
headerTitleStyle: {
color: ‘#fff’
}
}
}
);
const HomeNavigator = createStackNavigator(
{
Home: { screen: Home }
},
{
defaultNavigationOptions: ({navigation}) => ({
headerStyle: {
backgroundColor: ‘#5637DD’
},
headerTintColor: ‘#fff’,
headerTitleStyle: {
color: ‘#fff’
},
headerLeft: <Icon
name=’home’
type=’font-awesome’
iconStyle={styles.stackIcon}
onPress={() => navigation.toggleDrawer()}
/>
})
}
);
const AboutNavigator = createStackNavigator(
{
About: { screen: About }
},
{
defaultNavigationOptions: ({navigation}) => ({
headerStyle: {
backgroundColor: ‘#5637DD’
},
headerTintColor: ‘#fff’,
headerTitleStyle: {
color: ‘#fff’
},
headerLeft: <Icon
name=’info-circle’
type=’font-awesome’
iconStyle={styles.stackIcon}
onPress={() => navigation.toggleDrawer()}
/>
})
}
);
const ContactNavigator = createStackNavigator(
{
Contact: { screen: Contact }
},
{
defaultNavigationOptions: ({navigation}) => ({
headerStyle: {
backgroundColor: ‘#5637DD’
},
headerTintColor: ‘#fff’,
headerTitleStyle: {
color: ‘#fff’
},
headerLeft: <Icon
name=’address-card’
type=’font-awesome’
iconStyle={styles.stackIcon}
onPress={() => navigation.toggleDrawer()}
/>
})
}
);
const ReservationNavigator = createStackNavigator(
{
Reservation: { screen: Reservation }
},
{
defaultNavigationOptions: ({navigation}) => ({
headerStyle: {
backgroundColor: ‘#5637DD’
},
headerTintColor: ‘#fff’,
headerTitleStyle: {
color: ‘#fff’
},
headerLeft: <Icon
name=’tree’
type=’font-awesome’
iconStyle={styles.stackIcon}
onPress={() => navigation.toggleDrawer()}
/>
})
}
);
const FavoritesNavigator = createStackNavigator(
{
Favorites: { screen: Favorites }
},
{
defaultNavigationOptions: ({navigation}) => ({
headerStyle: {
backgroundColor: ‘#5637DD’
},
headerTintColor: ‘#fff’,
headerTitleStyle: {
color: ‘#fff’
},
headerLeft: <Icon
name=’heart’
type=’font-awesome’
iconStyle={styles.stackIcon}
onPress={() => navigation.toggleDrawer()}
/>
})
}
);
const CustomDrawerContentComponent = props => (
<ScrollView>
<SafeAreaView
style={styles.container}
forceInset={{top: ‘always’, horizontal: ‘never’}}>
<View style={styles.drawerHeader}>
<View style={{flex: 1}}>
<Image source={require(‘./images/logo.png’)} style={styles.drawerImage} />
</View>
<View style={{flex: 2}}>
<Text style={styles.drawerHeaderText}>NuCamp</Text>
</View>
</View>
<DrawerItems {…props} />
</SafeAreaView>
</ScrollView>
);
const MainNavigator = createDrawerNavigator(
{
Home: {
screen: HomeNavigator,
navigationOptions: {
drawerIcon: ({tintColor}) => (
<Icon
name=’home’
type=’font-awesome’
size={24}
color={tintColor}
/>
)
}
},
Directory: {
screen: DirectoryNavigator,
navigationOptions: {
drawerIcon: ({tintColor}) => (
<Icon
name=’list’
type=’font-awesome’
size={24}
color={tintColor}
/>
)
}
},
Reservation: {
screen: ReservationNavigator,
navigationOptions: {
drawerLabel: ‘Reserve Campsite’,
drawerIcon: ({tintColor}) => (
<Icon
name=’tree’
type=’font-awesome’
size={24}
color={tintColor}
/>
)
}
},
Favorites: {
screen: FavoritesNavigator,
navigationOptions: {
drawerLabel: ‘My Favorites’,
drawerIcon: ({tintColor}) => (
<Icon
name=’heart’
type=’font-awesome’
size={24}
color={tintColor}
/>
)
}
},
About: {
screen: AboutNavigator,
navigationOptions: {
drawerLabel: ‘About Us’,
drawerIcon: ({tintColor}) => (
<Icon
name=’info-circle’
type=’font-awesome’
size={24}
color={tintColor}
/>
)
}
},
Contact: {
screen: ContactNavigator,
navigationOptions: {
drawerLabel: ‘Contact Us’,
drawerIcon: ({tintColor}) => (
<Icon
name=’address-card’
type=’font-awesome’
size={24}
color={tintColor}
/>
)
}
}
},
{
drawerBackgroundColor: ‘#CEC8FF’,
contentComponent: CustomDrawerContentComponent
}
);
const AppNavigator = createAppContainer(MainNavigator)
class Main extends Component {
componentDidMount() {
this.props.fetchCampsites();
this.props.fetchComments();
this.props.fetchPromotions();
this.props.fetchPartners();
}
render() {
return (
<View style={{
flex: 1,
paddingTop: Platform.OS === ‘ios’ ? 0 : Expo.Constants.statusBarHeight
}}>
<AppNavigator />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
drawerHeader: {
backgroundColor: ‘#5637DD’,
height: 140,
alignItems: ‘center’,
justifyContent: ‘center’,
flex: 1,
flexDirection: ‘row’
},
drawerHeaderText: {
color: ‘#fff’,
fontSize: 24,
fontWeight: ‘bold’
},
drawerImage: {
margin: 10,
height: 60,
width: 60
},
stackIcon: {
marginLeft: 10,
color: ‘#fff’,
fontSize: 24
}
});
export default connect(null, mapDispatchToProps)(Main);
Swipe Option Buttons and Alerts
Swipe option buttons are commonly seen added to items on lists in mobile applications. Mobile devices have limited screen size and mobile apps often hide options in this and similar ways. When you swipe on an item in the configured direction, previously hidden option buttons are revealed. There are various ways to create swipe option buttons like this. We will use react-native-swipe-list-view library!
Alert is a way of popping up information in a dialog, usually to confirm some operation with the user – similar to a modal, but simpler. React Native has a built-in API we can use to easily create an Alert dialog with a title, text, and buttons that works both on Android and iOS. In the example show here, there are two buttons: one to cancel, and one to confirm (the OK button). Tapping on either button will dismiss the Alert. We can further attach event handlers to either or both buttons. We will configure the confirm button to dispatch an action to the Redux store to delete this campsite from the list of favorites
Swipe and Delete Favorites
- Install the react-native-swipe-list-view library to support a way to swipe to reveal option buttons: expo install react-native-swipe-list-view
ActionTypes.js
export const CAMPSITES_LOADING = ‘CAMPSITES_LOADING’;
export const ADD_CAMPSITES = ‘ADD_CAMPSITES’;
export const CAMPSITES_FAILED = ‘CAMPSITES_FAILED’;
export const ADD_COMMENTS = ‘ADD_COMMENTS’;
export const COMMENTS_FAILED = ‘COMMENTS_FAILED’;
export const PROMOTIONS_LOADING = ‘PROMOTIONS_LOADING’;
export const ADD_PROMOTIONS = ‘ADD_PROMOTIONS’;
export const PROMOTIONS_FAILED = ‘PROMOTIONS_FAILED’;
export const PARTNERS_LOADING = ‘PARTNERS_LOADING’;
export const ADD_PARTNERS = ‘ADD_PARTNERS’;
export const PARTNERS_FAILED = ‘PARTNERS_FAILED’;
export const ADD_FAVORITE = ‘ADD_FAVORITE’;
export const ADD_COMMENT = ‘ADD_COMMENT’;
export const DELETE_FAVORITE = ‘DELETE_FAVORITE’;
ActionCreators.js
import * as ActionTypes from ‘./ActionTypes’;
import { baseUrl } from ‘../shared/baseUrl’;
export const fetchComments = () => dispatch => {
return fetch(baseUrl + ‘comments’)
.then(response => {
if (response.ok) {
return response;
} else {
const error = new Error(`Error ${response.status}: ${response.statusText}`);
error.response = response;
throw error;
}
},
error => {
const errMess = new Error(error.message);
throw errMess;
})
.then(response => response.json())
.then(comments => dispatch(addComments(comments)))
.catch(error => dispatch(commentsFailed(error.message)));
};
export const commentsFailed = errMess => ({
type: ActionTypes.COMMENTS_FAILED,
payload: errMess
});
export const addComments = comments => ({
type: ActionTypes.ADD_COMMENTS,
payload: comments
});
export const fetchCampsites = () => dispatch => {
dispatch(campsitesLoading());
return fetch(baseUrl + ‘campsites’)
.then(response => {
if (response.ok) {
return response;
} else {
const error = new Error(`Error ${response.status}: ${response.statusText}`);
error.response = response;
throw error;
}
},
error => {
const errMess = new Error(error.message);
throw errMess;
})
.then(response => response.json())
.then(campsites => dispatch(addCampsites(campsites)))
.catch(error => dispatch(campsitesFailed(error.message)));
};
export const campsitesLoading = () => ({
type: ActionTypes.CAMPSITES_LOADING
});
export const campsitesFailed = errMess => ({
type: ActionTypes.CAMPSITES_FAILED,
payload: errMess
});
export const addCampsites = campsites => ({
type: ActionTypes.ADD_CAMPSITES,
payload: campsites
});
export const fetchPromotions = () => dispatch => {
dispatch(promotionsLoading());
return fetch(baseUrl + ‘promotions’)
.then(response => {
if (response.ok) {
return response;
} else {
const error = new Error(`Error ${response.status}: ${response.statusText}`);
error.response = response;
throw error;
}
},
error => {
const errMess = new Error(error.message);
throw errMess;
})
.then(response => response.json())
.then(promotions => dispatch(addPromotions(promotions)))
.catch(error => dispatch(promotionsFailed(error.message)));
};
export const promotionsLoading = () => ({
type: ActionTypes.PROMOTIONS_LOADING
});
export const promotionsFailed = errMess => ({
type: ActionTypes.PROMOTIONS_FAILED,
payload: errMess
});
export const addPromotions = promotions => ({
type: ActionTypes.ADD_PROMOTIONS,
payload: promotions
});
export const fetchPartners = () => dispatch => {
dispatch(partnersLoading());
return fetch(baseUrl + ‘partners’)
.then(response => {
if (response.ok) {
return response;
} else {
const error = new Error(`Error ${response.status}: ${response.statusText}`);
error.response = response;
throw error;
}
},
error => {
const errMess = new Error(error.message);
throw errMess;
})
.then(response => response.json())
.then(partners => dispatch(addPartners(partners)))
.catch(error => dispatch(partnersFailed(error.message)));
};
export const partnersLoading = () => ({
type: ActionTypes.PARTNERS_LOADING
});
export const partnersFailed = errMess => ({
type: ActionTypes.PARTNERS_FAILED,
payload: errMess
});
export const addPartners = partners => ({
type: ActionTypes.ADD_PARTNERS,
payload: partners
});
export const postFavorite = campsiteId => dispatch => {
setTimeout(() => {
dispatch(addFavorite(campsiteId));
}, 2000);
};
export const addFavorite = campsiteId => ({
type: ActionTypes.ADD_FAVORITE,
payload: campsiteId
});
export const postComment = (campsiteId, rating, author, text) => dispatch => {
const newComment = {
campsiteId: campsiteId,
rating: rating,
author: author,
text: text
};
newComment.date=new Date().toDateString();
setTimeout(() => {
dispatch(addComment(newComment));
}, 2000);
}
export const addComment = comments => ({
type: ActionTypes.ADD_COMMENT,
payload: comments
});
export const deleteFavorite = campsiteId => ({
type: ActionTypes.DELETE_FAVORITE,
payload: campsiteId
});
favorites.js
import * as ActionTypes from ‘./ActionTypes’;
export const favorites = (state = [], action) => {
switch (action.type) {
case ActionTypes.ADD_FAVORITE:
if (state.includes(action.payload)) {
return state;
}
return state.concat(action.payload);
case ActionTypes.DELETE_FAVORITE:
return state.filter(favorite => favorite !== action.payload);
default:
return state;
}
};
FavoritesComponent.js
import React, { Component } from ‘react’;
import { FlatList, View, Text, StyleSheet } from ‘react-native’;
import { ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { SwipeRow } from ‘react-native-swipe-list-view’;
import { TouchableOpacity } from ‘react-native-gesture-handler’;
import { deleteFavorite } from ‘../redux/ActionCreators’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
favorites: state.favorites
};
};
const mapDispatchToProps = {
deleteFavorite: campsiteId => deleteFavorite(campsiteId)
};
class Favorites extends Component {
static navigationOptions = {
title: ‘My Favorites’
}
render() {
const { navigate } = this.props.navigation;
const renderFavoriteItem = ({item}) => {
return (
<SwipeRow rightOpenValue={-100} style={styles.swipeRow}>
<View style={styles.deleteView}>
<TouchableOpacity
style={styles.deleteTouchable}
onPress={() => this.props.deleteFavorite(item.id)}
>
<Text style={styles.deleteText}>Delete</Text>
</TouchableOpacity>
</View>
<View>
<ListItem
title={item.name}
subtitle={item.description}
leftAvatar={{source: {uri: baseUrl + item.image}}}
onPress={() => navigate(‘CampsiteInfo’, {campsiteId: item.id})}
/>
</View>
</SwipeRow>
);
};
if (this.props.campsites.isLoading) {
return <Loading />;
}
if (this.props.campsites.errMess) {
return (
<View>
<Text>{this.props.campsites.errMess}</Text>
</View>
);
}
return (
<FlatList
data={this.props.campsites.campsites.filter(
campsite => this.props.favorites.includes(campsite.id)
)}
renderItem={renderFavoriteItem}
keyExtractor={item => item.id.toString()}
/>
);
}
}
const styles = StyleSheet.create({
deleteView: {
flexDirection: ‘row’,
justifyContent: ‘flex-end’,
alignItems: ‘center’,
flex: 1
},
deleteTouchable: {
backgroundColor: ‘red’,
height: ‘100%’,
justifyContent: ‘center’
},
deleteText: {
color: ‘white’,
fontWeight: ‘700’,
textAlign: ‘center’,
fontSize: 16,
width: 100
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Favorites);
Alerts
FavoritesComponent.js
import React, { Component } from ‘react’;
import { FlatList, View, Text, StyleSheet, Alert } from ‘react-native’;
import { ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { SwipeRow } from ‘react-native-swipe-list-view’;
import { TouchableOpacity } from ‘react-native-gesture-handler’;
import { deleteFavorite } from ‘../redux/ActionCreators’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
favorites: state.favorites
};
};
const mapDispatchToProps = {
deleteFavorite: campsiteId => deleteFavorite(campsiteId)
};
class Favorites extends Component {
static navigationOptions = {
title: ‘My Favorites’
}
render() {
const { navigate } = this.props.navigation;
const renderFavoriteItem = ({item}) => {
return (
<SwipeRow rightOpenValue={-100} style={styles.swipeRow}>
<View style={styles.deleteView}>
<TouchableOpacity
style={styles.deleteTouchable}
onPress={() =>
Alert.alert(
‘Delete Favorite?’,
‘Are you sure you wish to delete the favorite campsite ‘ +
item.name +
‘?’,
[
{
text: ‘Cancel’,
onPress: () => console.log(item.name + ‘Not Deleted’),
style: ‘cancel’
},
{
text: ‘OK’,
onPress: () => this.props.deleteFavorite(item.id)
},
],
{ cancelable: false }
)
}
>
<Text style={styles.deleteText}>Delete</Text>
</TouchableOpacity>
</View>
<View>
<ListItem
title={item.name}
subtitle={item.description}
leftAvatar={{source: {uri: baseUrl + item.image}}}
onPress={() => navigate(‘CampsiteInfo’, {campsiteId: item.id})}
/>
</View>
</SwipeRow>
);
};
if (this.props.campsites.isLoading) {
return <Loading />;
}
if (this.props.campsites.errMess) {
return (
<View>
<Text>{this.props.campsites.errMess}</Text>
</View>
);
}
return (
<FlatList
data={this.props.campsites.campsites.filter(
campsite => this.props.favorites.includes(campsite.id)
)}
renderItem={renderFavoriteItem}
keyExtractor={item => item.id.toString()}
/>
);
}
}
const styles = StyleSheet.create({
deleteView: {
flexDirection: ‘row’,
justifyContent: ‘flex-end’,
alignItems: ‘center’,
flex: 1
},
deleteTouchable: {
backgroundColor: ‘red’,
height: ‘100%’,
justifyContent: ‘center’
},
deleteText: {
color: ‘white’,
fontWeight: ‘700’,
textAlign: ‘center’,
fontSize: 16,
width: 100
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Favorites);
Animations
Subtle animations added to your application can go a long way in enhancing the user experience. React Native includes a powerful, declarative library called Animated. The Animated library is designed to make animations fluid, powerful, and painless to build and maintain. The most basic workflow for creating an animation is to create an Animated.Value, hook it up to one or more style attributes of an animated component, and then drive updates via animations using Animated.timing()
Components must be configured to be able to use the animated library. There is a wrapper function called create animated component that can be used to make a component animatable. You can use this on any component. But six of the most common animated components are already exported for you to use with this wrapper function as:
- Animated.Image
- Animated.ScrollView
- Animated.Text
- and so on!
Add an animation to the Home component, using React Native’s Animated API.
HomeComponent.js
import React, { Component } from ‘react’;
import { View, Text, Animated } from ‘react-native’;
import { Card } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import Loading from ‘./LoadingComponent’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
promotions: state.promotions,
partners: state.partners
};
};
function RenderItem(props) {
const {item} = props;
if (props.isLoading) {
return <Loading />;
}
if (props.errMess) {
return (
<View>
<Text>{props.errMess}</Text>
</View>
);
}
if (item) {
return (
<Card
featuredTitle={item.name}
image={{uri: baseUrl + item.image}}>
<Text style={{margin: 10}}>
{item.description}
</Text>
</Card>
);
}
return <View />;
}
class Home extends Component {
constructor(props) {
super(props);
this.state = {
scaleValue: new Animated.Value(0)
};
}
animate() {
Animated.timing(
this.state.scaleValue,
{
toValue: 1,
duration: 1500,
useNativeDriver: true
}
).start();
}
componentDidMount() {
this.animate();
}
static navigationOptions = {
title: ‘Home’
}
static navigationOptions = {
title: ‘Home’
}
render() {
return (
<Animated.ScrollView style={{transform: [{scale: this.state.scaleValue}]}}>
<RenderItem
item={this.props.campsites.campsites.filter(campsite => campsite.featured)[0]}
isLoading={this.props.campsites.isLoading}
errMess={this.props.campsites.errMess}
/>
<RenderItem
item={this.props.promotions.promotions.filter(promotion => promotion.featured)[0]}
isLoading={this.props.promotions.isLoading}
errMess={this.props.promotions.errMess}
/>
<RenderItem
item={this.props.partners.partners.filter(partner => partner.featured)[0]}
isLoading={this.props.partners.isLoading}
errMess={this.props.partners.errMess}
/>
</Animated.ScrollView>
);
}
}
export default connect(mapStateToProps)(Home);
The Animated API provides a fairly comprehensive way of doing complex animations, and comes included with the React Native core. But it can be quite cumbersome – a lot of setup for a simple animation. There are many third party libraries built on top of the Animated library to more easily set up basic animations. One is react native animatable which we will be using.
Animatable
- First, install react-native-animatable in your nucampsite folder: expo install react-native-animatable
AboutComponent.js
import React, { Component } from ‘react’;
import { FlatList, Text, ScrollView } from ‘react-native’;
import { Card, ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import Loading from ‘./LoadingComponent’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
return {
partners: state.partners
};
};
class About extends Component {
static navigationOptions = {
title: ‘About Us’
}
render() {
const renderPartner = ({item}) => {
return (
<ListItem
title={item.name}
subtitle={item.description}
leftAvatar={{source: {uri: baseUrl + item.image}}}
/>
);
};
if (this.props.partners.isLoading) {
return (
<ScrollView>
<Mission />
<Card
title=’Community Partners’>
<Loading />
</Card>
</ScrollView>
);
}
if (this.props.partners.errMess) {
return (
<ScrollView>
<Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
<Mission />
<Card
title=’Community Partners’>
<Text>{this.props.partners.errMess}</Text>
</Card>
</Animatable.View>
</ScrollView>
);
}
return (
<ScrollView>
<Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
<Card title=’Mission’>
<Text wrapperStyle={{margin: 10}}>
We present a curated database of the best campsites in the vast woods and backcountry of the World Wide Web Wilderness. We increase access to adventure for the public while promoting safe and respectful use of resources. The expert wilderness trekkers on our staff personally verify each campsite to make sure that they are up to our standards. We also present a platform for campers to share reviews on campsites they have visited with each other.
</Text>
</Card>
<Card title=’Community Partners’>
<FlatList
data={this.props.partners.partners}
renderItem={renderPartner}
keyExtractor={item => item.id.toString()}
/>
</Card>
</Animatable.View>
</ScrollView>
);
}
}
export default connect(mapStateToProps)(About);
CampsiteInfoComponent.js
import React, { Component } from ‘react’;
import { Text, View, ScrollView, FlatList, Modal, Button, StyleSheet } from ‘react-native’;
import { Card, Icon, Rating, Input } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import { postFavorite, postComment } from ‘../redux/ActionCreators’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
comments: state.comments,
favorites: state.favorites
};
};
const mapDispatchToProps = {
postFavorite: campsiteId => (postFavorite(campsiteId)),
postComment: (campsiteId, rating, author, text) => postComment(campsiteId, rating, author, text)
};
function RenderCampsite(props) {
const {campsite} = props;
if (campsite) {
return (
<Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
<Card
featuredTitle={campsite.name}
image={{uri: baseUrl + campsite.image}}>
<Text style={{margin: 10}}>
{campsite.description}
</Text>
<View style={styles.cardRow}>
<Icon
name={props.favorite ? ‘heart’ : ‘heart-o’}
type=’font-awesome’
color=’#f50′
raised
reverse
onPress={() => props.favorite ?
console.log(‘Already set as a favorite’) : props.markFavorite()}
/>
<Icon
name=’pencil’
type=’font-awesome’
color=’#5637DD’
raised
reverse
onPress={() => props.onShowModal()}
// console.log(‘Already set as a favorite’) : props.markFavorite()}
/>
</View>
</Card>
</Animatable.View>
);
}
return <View />;
}
function RenderComments({comments}) {
const renderCommentItem = ({item}) => {
return (
<View style={{margin: 10}}>
<Text style={{fontSize: 14}}>{item.text}</Text>
<Rating
startingValue={item.rating}
imageSize={10}
style={{paddingVertical: ‘5%’, alignItems: ‘flex-start’}}
readonly
/>
<Text style={{fontSize: 12}}>{`– ${item.author}, ${item.date}`}</Text>
</View>
);
};
return (
<Animatable.View animation=’fadeInUp’ duration={2000} delay={1000}>
<Card title=’Comments’>
<FlatList
data={comments}
renderItem={renderCommentItem}
keyExtractor={item => item.id.toString()}
/>
</Card>
</Animatable.View>
);
}
class CampsiteInfo extends Component {
constructor(props) {
super(props);
this.state = {
showModal: false,
rating: 5,
author: ” “,
text: ” “
};
}
toggleModal() {
this.setState({showModal: !this.state.showModal});
}
handleComment(campsiteId) {
this.props.postComment(campsiteId, this.state.rating, this.state.author, this.state.text);
this.toggleModal();
}
resetForm() {
this.setState({
showModal: false,
rating: 5,
author: ” “,
text: ” “
});
}
markFavorite(campsiteId) {
this.props.postFavorite(campsiteId);
}
static navigationOptions = {
title: ‘Campsite Information’
}
render() {
const campsiteId = this.props.navigation.getParam(‘campsiteId’);
const campsite = this.props.campsites.campsites.filter(campsite => campsite.id === campsiteId)[0];
const comments = this.props.comments.comments.filter(comment => comment.campsiteId === campsiteId);
return (
<ScrollView>
<RenderCampsite campsite={campsite}
favorite={this.props.favorites.includes(campsiteId)}
markFavorite={() => this.markFavorite(campsiteId)}
onShowModal={() => this.toggleModal(campsiteId)}
/>
<RenderComments comments={comments} />
<Modal
animationType={‘slide’}
transparent={false}
visible={this.state.showModal}
onRequestClose={() => this.toggleModal()}
>
<View style={styles.modal}>
<Rating
showRating
startingValue={this.state.rating}
imageSize={40}
onFinishRating={rating => this.setState({rating: rating})}
style={{paddingVertical: 10}}
/>
<Input
placeholder=’Author’
leftIcon={{ type: “font-awesome”, name: “user-o”}}
leftIconContainerStyle={{paddingRight: 10}}
onChangeText={value => this.setState({author: value})}
value={this.state.author}
/>
<Input
placeholder=’Comment’
leftIcon={{ type: “font-awesome”, name: “comment-o”}}
leftIconContainerStyle={{paddingRight: 10}}
onChangeText={value => this.setState({text: value})}
value={this.state.text}
/>
<View style={{margin: 10}}>
<Button
onPress={() => {
this.handleComment(campsiteId);
this.resetForm();
}}
color=’#5637DD’
title=’Submit’
/>
</View>
<View style={{margin: 10}}>
<Button
onPress={() => {
this.toggleModal();
this.resetForm();
}}
color=’#808080′
title=’Cancel’
/>
</View>
</View>
</Modal>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
cardRow: {
alignItems: ‘center’,
justifyContent: ‘center’,
flex: 1,
flexDirection: ‘row’,
margin: 20
},
modal: {
justifyContent: ‘center’,
margin: 20
}
});
export default connect(mapStateToProps, mapDispatchToProps)(CampsiteInfo);
ContactComponent.js
import React, { Component } from ‘react’;
import { View, Text, ScrollView } from ‘react-native’;
import { Card } from ‘react-native-elements’;
import * as Animatable from ‘react-native-animatable’;
class Contact extends Component {
static navigationOptions = {
title: ‘Contact Us’
}
render() {
return (
<ScrollView>
<Animatable.View animation=’fadeInDown’ duration={2000} delay={1000}>
<Card title=’Contact Information’ wrapperStyle={{margin: 20}}>
<Text>1 Nucamp Way </Text>
<Text>Seattle, WA 98001</Text>
<Text style={{marginBottom: 10}} >U.S.A.</Text>
<Text>Phone: 1-206-555-1234 </Text>
<Text>Email: campsites@nucamp.co</Text>
</Card>
</Animatable.View>
</ScrollView>
);
}
}
export default Contact;
DirectoryComponent.js
import React, { Component } from ‘react’;
import { View, FlatList, Text } from ‘react-native’;
import { Tile } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { baseUrl } from ‘../shared/baseUrl’;
import Loading from ‘./LoadingComponent’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
};
};
class Directory extends Component {
static navigationOptions = {
title: ‘Directory’
}
render() {
const { navigate } = this.props.navigation;
const renderDirectoryItem = ({item}) => {
return (
<Animatable.View animation=’fadeInRightBig’ duration={2000}>
<Tile
title={item.name}
subtitle={item.description}
featured
onPress={() => navigate(‘CampsiteInfo’, { campsiteId: item.id })}
imageSrc={{uri: baseUrl + item.image}}
/>
</Animatable.View>
);
};
if (this.props.campsites.isLoading) {
return <Loading />;
}
if (this.props.campsites.errMess) {
return (
<View>
<Text>{this.props.campsites.errMess}</Text>
</View>
);
}
return (
<FlatList
data={this.props.campsites.campsites}
renderItem={renderDirectoryItem}
keyExtractor={item => item.id.toString()}
/>
);
}
}
export default connect(mapStateToProps)(Directory);
FavoritesComponent.js
import React, { Component } from ‘react’;
import { FlatList, View, Text, StyleSheet, Alert } from ‘react-native’;
import { ListItem } from ‘react-native-elements’;
import { connect } from ‘react-redux’;
import { Loading } from ‘./LoadingComponent’;
import { baseUrl } from ‘../shared/baseUrl’;
import { SwipeRow } from ‘react-native-swipe-list-view’;
import { TouchableOpacity } from ‘react-native-gesture-handler’;
import { deleteFavorite } from ‘../redux/ActionCreators’;
import * as Animatable from ‘react-native-animatable’;
const mapStateToProps = state => {
return {
campsites: state.campsites,
favorites: state.favorites
};
};
const mapDispatchToProps = {
deleteFavorite: campsiteId => deleteFavorite(campsiteId)
};
class Favorites extends Component {
static navigationOptions = {
title: ‘My Favorites’
}
render() {
const { navigate } = this.props.navigation;
const renderFavoriteItem = ({item}) => {
return (
<SwipeRow rightOpenValue={-100} style={styles.swipeRow}>
<View style={styles.deleteView}>
<TouchableOpacity
style={styles.deleteTouchable}
onPress={() =>
Alert.alert(
‘Delete Favorite?’,
‘Are you sure you wish to delete the favorite campsite ‘ +
item.name +
‘?’,
[
{
text: ‘Cancel’,
onPress: () => console.log(item.name + ‘Not Deleted’),
style: ‘cancel’
},
{
text: ‘OK’,
onPress: () => this.props.deleteFavorite(item.id)
},
],
{ cancelable: false }
)
}
>
<Text style={styles.deleteText}>Delete</Text>
</TouchableOpacity>
</View>
<View>
<ListItem
title={item.name}
subtitle={item.description}
leftAvatar={{source: {uri: baseUrl + item.image}}}
onPress={() => navigate(‘CampsiteInfo’, {campsiteId: item.id})}
/>
</View>
</SwipeRow>
);
};
if (this.props.campsites.isLoading) {
return <Loading />;
}
if (this.props.campsites.errMess) {
return (
<View>
<Text>{this.props.campsites.errMess}</Text>
</View>
);
}
return (
<Animatable.View animation=’fadeInRightBig’ duration={2000}>
<FlatList
data={this.props.campsites.campsites.filter(
campsite => this.props.favorites.includes(campsite.id)
)}
renderItem={renderFavoriteItem}
keyExtractor={item => item.id.toString()}
/>
</Animatable.View>
);
}
}
const styles = StyleSheet.create({
deleteView: {
flexDirection: ‘row’,
justifyContent: ‘flex-end’,
alignItems: ‘center’,
flex: 1
},
deleteTouchable: {
backgroundColor: ‘red’,
height: ‘100%’,
justifyContent: ‘center’
},
deleteText: {
color: ‘white’,
fontWeight: ‘700’,
textAlign: ‘center’,
fontSize: 16,
width: 100
}
});
export default connect(mapStateToProps, mapDispatchToProps)(Favorites);
Additional Resources: