- implementing immutable.js

This commit is contained in:
2019-07-17 03:21:23 -04:00
parent 8ecf036cc4
commit 434a1ded24
39 changed files with 1123 additions and 187 deletions

View File

@@ -7,46 +7,10 @@
*/
import React, {Fragment} from 'react';
import {
SafeAreaView,
StyleSheet,
ScrollView,
View,
Text,
StatusBar,
} from 'react-native';
import { createAppContainer } from 'react-navigation';
import { Tabs } from './router.js';
const App = () => {
return <Tabs />;
};
const styles = StyleSheet.create({
scrollView: {
backgroundColor: Colors.lighter,
},
body: {
backgroundColor: Colors.white,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
color: Colors.black,
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
color: Colors.dark,
},
highlight: {
fontWeight: '700',
},
});
const App = createAppContainer(Tabs);
export default App;

View File

@@ -1,29 +1,82 @@
import { List } from 'immutable';
import { getEndpointUrl } from '../api/index.js';
import Auction from '../domain/Auction.js';
import Event from '../domain/Event.js';
import Item from '../domain/Item.js';
import {
AUCTIONS_UPDATED,
BLOCK_UI,
EVENTS_LOADED,
GET_EVENTS,
GET_ITEMS,
ITEMS_LOADED,
UNBLOCK_UI,
} from '../constants/actionTypes.js';
import { API_ENDPOINTS } from '../constants/constants.js';
export const getEvents = () => (dispatch) => {
return fetch(getEndpointUrl[API_ENDPOINTS.GET_EVENTS])
dispatch(blockUI());
fetch(getEndpointUr(API_ENDPOINTS.GET_EVENTS))
.then(response => response.json())
.then(payload => {
dispatch({ type: EVENTS_LOADED, payload });
.then((payload) => {
const events = List(payload).map((i) => Event.fromJS(i));
dispatch({ type: EVENTS_LOADED, payload: events });
dispatch(unblockUI);
});
};
export const getItems = () => (dispatch, getState) => {
const { activeEvent } = getState();
let apiUrl = getEndpointUrl[API_ENDPOINTS.GET_ITEMS];
apiUrl = apiUrl.replace(/:event_id/, activeEvent);
const state = getState();
const activeEvent = state.get('activeEvent');
let apiUrl = getEndpointUrl(API_ENDPOINTS.GET_ITEMS);
apiUrl = apiUrl.replace(/:event_id$/, '');
if (activeEvent) {
apiUrl = `${apiUrl}${activeEvent}`;
}
dispatch(blockUI());
fetch(apiUrl)
.then(response => response.json())
.then(payload => {
const items = List(payload).map(i => Item.fromJS(i));
dispatch({ type: ITEMS_LOADED, payload: items });
dispatch(unblockUI());
})
.catch(err => console.error('[actions::getItems]', err));
};
export const getStatus = () => (dispatch, getState) => {
const state = getState();
const activeEvent = state.get('activeEvent');
let apiUrl = getEndpointUrl(API_ENDPOINTS.GET_STATUS);
apiUrl = apiUrl.replace(/:event_id$/, '');
if (activeEvent) {
apiUrl = `${apiUrl}${activeEvent}`;
}
dispatch(blockUI());
return fetch(apiUrl)
.then(response => response.json())
.then(payload => {
dispatch({ type: ITEMS_LOADED, payload });
});
const auctions = List(payload).map(i => Auction.fromJS(i));
dispatch(unblockUI());
dispatch({ type: AUCTIONS_UPDATED, payload: auctions });
})
.catch(err => console.error('[actions::getStatus]', err));
};
export const blockUI = () => ({
type: BLOCK_UI,
});
export const unblockUI = () => ({
type: UNBLOCK_UI,
});

View File

@@ -3,9 +3,11 @@ const apiUrl = 'http://localhost:3001';
const endpoints = {
// Events and Items
GET_EVENTS: '/events',
GET_ITEMS: '/items?eventId=:event_id',
// GET_ITEMS: '/items?eventId=:event_id',
GET_ITEMS: '/items',
// Auction Interactions
// GET_STATUS: '/auction/:event_id',
GET_STATUS: '/auction',
PLACE_BID: '/bids/:item_id',
PURCHASE_ITEM: '/sales',
@@ -30,8 +32,8 @@ const cacheBuster = () => {
export const getEndpointUrl = (endpoint) => {
if (!endpoints[endpoint]) {
return throw new Error('Invalid API endpoint specified');
throw new Error('Invalid API endpoint specified');
}
return `${apiUrl}${endpoints[endpoint]}${cacheBuster()}`;
return `${apiUrl}${endpoints[endpoint]}`; //`${cacheBuster()}`;
};

View File

@@ -17,8 +17,15 @@ const AuctionPriceAndBidCount = ({ bidCount, currentPrice }) => {
};
AuctionPriceAndBidCount.propTypes = {
itemId: PropTypes.string.isRequired,
bidCount: PropTypes.number.isRequired,
currentPrice: PropTypes.number.isRequired,
};
const styles = StyleSheet.create({
currentPriceAndBidCount: {
color: '#000',
},
});
export default AuctionPriceAndBidCount;

View File

@@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
import {
StyleSheet,
View,
Text,
} from 'react-native';
const BidStatus = ({ bidCount, currentPrice, isBidding, isWinning }) => {
const BidStatus = ({ isBidding, isWinning }) => {
if (!isBidding) {
return null;
}
@@ -17,16 +17,16 @@ const BidStatus = ({ bidCount, currentPrice, isBidding, isWinning }) => {
return (
<Text style={statusBarStyle} numberOfLines={1}>
{`${currentPrice} (${bidCount} bids)`}
{isWinning && `Oh no! You have been outbid!`}
{!isWinning && isBidding && `You have the winning bid! (for now...)`}
</Text>
);
};
BidStatus.propTypes = {
bidCount: PropTypes.number.isRequired,
currentPrice: PropTypes.number.isRequired,
isBidding: PropTypes.bool.isRequired,
isWinning: PropTypes.bool.isRequired,
itemId: PropTypes.string.isRequired,
};
const styles = StyleSheet.create({

View File

@@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
View,
} from 'react-native';
const FilterBar = ({ changeFilterer, changeViewMode, filterMode, viewMode }) => (
<View style={styles.filterBar}>
<Text style={styles.filter}>Filter</Text>
<Text style={styles.view}>View</Text>
</View>
);
FilterBar.propTypes = {
changeFilterer: PropTypes.func.isRequired,
changeViewMode: PropTypes.func.isRequired,
filterMode: PropTypes.string,
viewMode: PropTypes.string,
};
FilterBar.defaultProps = {
filterMode: null,
viewMode: null,
};
const styles = StyleSheet.create({
filterBar: {
backgroundColor: '#0F0',
flexDirection: 'row',
},
filter: {
flex: 2,
},
view: {
flex: 2,
},
});
export default FilterBar;

View File

@@ -1,4 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
TouchableOpacity,
@@ -7,6 +9,8 @@ import {
View
} from 'react-native';
import GallerySwiper from 'react-native-gallery-swiper';
import AuctionPriceAndBidCount from '../../containers/Auction/AuctionPriceAndBidCount.js';
import BidStatus from '../../containers/Auction/BidStatus.js';
@@ -25,21 +29,34 @@ export default class ItemRow extends Component {
url: PropTypes.string,
}),
),
start: PropTytpes.string.isRequired,
startPrice: PropTypes.number,
start: PropTypes.string.isRequired,
startingPrice: PropTypes.number.isRequired,
subtitle: PropTypes.string,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
}
static get defaultProps() {
return {
description: null,
donor: null,
images: null,
subtitle: null,
};
}
constructor(props) {
super(props);
}
_getBidTime = () => {
const { end, start } = this.props;
return getAuctionTime({ end, start });
}
_viewItemDetail = () => {
const { id } = this.props.details;
const { _id: id } = this.props.details;
this.props.navigation.navigate('Item', { id });
}
@@ -48,9 +65,10 @@ export default class ItemRow extends Component {
description,
donor,
end,
id,
images,
start,
startPrice,
startingPrice,
subtitle,
title,
type,
@@ -59,13 +77,18 @@ export default class ItemRow extends Component {
return(
<TouchableOpacity onPress={this._viewItemDetail}>
<View style={styles.rowContainer}>
<Image
source={{uri: images[0].url}}
style={styles.image}
resizeMode="contain"
/>
{images !== null && images.length > 0 && (
<GallerySwiper
enableScale={false}
images={images}
initialNumToRender={2}
resizeMode="cover"
sensitiveScroll={false}
style={{height: 280}}
/>
)}
<View style={styles.rowText}>
{type === ITEM_TYPES.AUCTION && <BidStatus id={item.id} />}
{type === ITEM_TYPES.AUCTION && <BidStatus itemId={id} />}
<Text style={styles.title} numberOfLines={2} ellipsizeMode={'tail'}>
{title}
</Text>
@@ -78,10 +101,10 @@ export default class ItemRow extends Component {
</Text>
)}
{type === ITEM_TYPES.AUCTION ? (
<AuctionPriceAndBidCount id={item.id} />
<AuctionPriceAndBidCount itemId={id} />
) : (
<Text style={styles.price} numberOfLines={1} ellipsizeMode={'tail'}>
{formatPrice(startPrice)}
{formatPrice(startingPrice)}
</Text>
)}
<Text style={styles.timeline} numberOfLines={1}>
@@ -126,14 +149,17 @@ const styles = StyleSheet.create({
rowContainer: {
backgroundColor: '#FFF',
borderRadius: 4,
flexDirection: 'row',
height: 100,
flex: 1,
flexDirection: 'column',
marginRight: 10,
marginLeft: 10,
marginTop: 10,
padding: 10,
shadowColor: '#CCC',
shadowOffset:{ width: 1, height: 1, },
shadowOffset: {
width: 1,
height: 1
},
shadowOpacity: 1.0,
shadowRadius: 1,
},

View File

@@ -7,6 +7,7 @@ export const ITEMS_LOADED = 'ITEMS_LOADED';
export const ITEMS_LOAD_FAILED = 'ITEMS_LOAD_FAILED';
export const UPDATE_AUCTIONS = 'UPDATE_AUCTIONS';
export const AUCTIONS_UPDATED = 'AUCTIONS_UPDATED';
export const SET_TICKET_PURCHASE_FLOW = 'SET_TICKET_PURCHASE_FLOW';
@@ -34,6 +35,7 @@ export const LINK_FACEBOOK_SUCCESS = 'LINK_FACEBOOK_SUCCESS';
export const LINK_GOOGLE_FAILURE = 'LINK_GOOGLE_FAILURE';
export const LINK_GOOGLE_SUCCESS = 'LINK_GOOGLE_SUCCESS';
export const SET_PROFILE = 'SET_PROFILE';
export const UPDATE_PROFILE = 'UPDATE_PROFILE';
export const SET_NOM_DE_BID = 'SET_NOM_DE_BID';
@@ -41,3 +43,9 @@ export const SET_PASSWORD = 'SET_PASSWORD';
export const ADD_PAYMENT_DATA = 'ADD_PAYMENT_DATA';
export const DO_CHECKOUT = 'DO_CHECKOUT';
export const BLOCK_UI = 'BLOCK_UI';
export const UNBLOCK_UI = 'UNBLOCK_UI';
export const SET_ACTIVE_EVENT = 'SET_ACTIVE_EVENT';
export const UNSET_ACTIVE_EVENT = 'UNSET_ACTIVE_EVENT';

18
app/containers/Auction.js Normal file
View File

@@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { getItems, getStatus } from '../actions/index.js';
import { getAuctionItemsAsList } from '../selectors/items.js';
import Auction from '../screens/Auction.js';
const matchStateToProps = (state) => ({
items: getAuctionItemsAsList(state),
});
const mapDispatchToProps = (dispatch) => ({
fetchItems: () => dispatch(getItems(dispatch)),
fetchStatus: () => dispatch(getStatus(dispatch)),
});
export default connect(matchStateToProps, mapDispatchToProps)(Auction);

View File

@@ -1,13 +1,15 @@
import { connect } from 'react-redux';
import { getItemBidCount, getItemPrice } from '../../selectors/auctions.js';
import AuctionPriceAndBidCount from '../../components/Auction/AuctionPriceAndBidCount.js';
function mapStateToProps(state, ownProps) {
const { bidCount, currentPrice } = getAuctionItemStatus(state, ownProps.id);
const { itemId } = ownProps;
return {
bidCount,
currentPrice,
bidCount: getItemBidCount(state, itemId),
currentPrice: getItemPrice(state, itemId),
};
}

View File

@@ -1,20 +1,15 @@
import { connect } from 'react-redux';
import { isBiddingItem, isWinningItem } from '../../selectors/auctions.js';
import AuctionPriceAndBidCount from '../../components/Auction/BidStatus.js';
function mapStateToProps(state, ownProps) {
const {
bidCount,
currentPrice,
isBidding,
isWinning,
} = getAuctionItemStatus(state, ownProps.id);
const { itemId } = ownProps;
return {
bidCount,
currentPrice,
isBidding,
isWinning,
isBidding: isBiddingItem(state, itemId),
isWinning: isWinningItem(state, itemId),
};
}

16
app/domain/Auction.js Normal file
View File

@@ -0,0 +1,16 @@
import { Record } from 'immutable';
export default class Auction extends Record({
id: null,
bidCount: 0,
isBidding: false,
isWinning: false,
itemPrice: 0,
}) {}
Auction.fromJS = (data = {}) => {
return new Auction({
id: data._id,
...data,
});
};

39
app/domain/Event.js Normal file
View File

@@ -0,0 +1,39 @@
import { List, Record } from 'immutable';
import Post from './Post.js';
import TicketClass from './TicketClass.js';
export default class Event extends Record({
id: null,
isTicketed: false,
requireLoginToSeeAuction: false,
description: null,
endTime: null,
images: new List(),
posts: new List(),
showFrom: null,
showUntil: null,
startTime: null,
tagline: null,
title: null,
url: null,
ticketClasses: new List(),
}) {
get isSoldOut() {
if (this.isTicketed) {
return false;
}
return this.ticketClasses.find(t => t.available > 0) || false;
}
}
Event.fromJS = (data = {}) => {
return new Event({
id: data._id,
...data,
images: new List(data.images),
posts: new List(data.posts.map(p => Post.fromJS(p))),
ticketClasses: new List(data.ticketClasses.map(t => TicketClass.fromJS(t))),
});
};

43
app/domain/Item.js Normal file
View File

@@ -0,0 +1,43 @@
import { List, Record } from 'immutable';
export default class Item extends Record({
bidCount: 0,
bidIncrement: 10,
catalogNumber: null,
currentPrice: 0,
description: null,
donor: null,
end: null,
estimatedValue: null,
eventId: null,
hideAfterEnd: false,
hideBeforeStart: false,
id: null,
images: new List(),
isShippable: false,
notifyOnAvailable: false,
quantityAvailable: 1,
soldCount: 0,
start: null,
startingPrice: null,
subtitle: null,
title: null,
type: null,
shippingCost: 0,
}) {
get isSoldOut() {
return this.quantityAvailable > this.soldCount;
}
get totalWithShipping() {
return this.currentPrice + this.shippingCost;
}
}
Item.fromJS = (data = {}) => {
return new Item({
id: data._id,
...data,
images: List(data.images),
});
};

20
app/domain/Post.js Normal file
View File

@@ -0,0 +1,20 @@
import { Record } from 'immutable';
export default class Post extends Record({
author: null,
content: null,
id: null,
isPublic: false,
scheduledPost: false,
sendNotification: false,
timestamp: null,
title: null,
}) {};
Post.fromJS = (data = {}) => {
return new TicketClass({
id: data._id,
...data,
});
};

39
app/domain/Profile.js Normal file
View File

@@ -0,0 +1,39 @@
import { List, Record } from 'immutable';
export default class Profile extends Record({
addresses: new List(),
avatar: null,
email: null,
firstName: null,
generatedNomDeBid: false,
hasLinkedApple: false,
hasLinkedFacebook: false,
hasLinkedGoogle: false,
hasLocalAccount: false,
id: null,
isAllowedToBid: false,
isOrganizationEmployee: false,
isVerified: false,
lastName: null,
nomDeBid: null,
organizationIdentifier: null,
paymentToken: null,
phones: new List(),
}) {
get canBid() {
return this.isAllowedToBid && this.paymentToken !== null;
}
get fullName() {
return `${this.firstName} ${this.lastName}`;
}
}
Profile.fromJS = (data = {}) => {
return new Profile({
id: data._id,
...data,
addresses: new List(data.addresses),
phones: new List(data.phones),
});
};

27
app/domain/TicketClass.js Normal file
View File

@@ -0,0 +1,27 @@
import { List, Record } from 'immutable';
export default class TicketClass extends Record({
available: 0,
capacity: 0,
endSale: null,
id: null,
itemId: null,
name: null,
price: 0,
startSale: null,
}) {
get isAlmostGone() {
return this.available < (this.capacity * 0.20);
}
get isSoldOut() {
return this.available === 0;
}
}
TicketClass.fromJS = (data = {}) => {
return new TicketClass({
id: data._id,
...data,
});
};

View File

@@ -0,0 +1,12 @@
import { SET_ACTIVE_EVENT, UNSET_ACTIVE_EVENT } from '../constants/actionTypes.js';
export const activeEvent = (state = null, action) => {
switch (action.type) {
case SET_ACTIVE_EVENT:
return action.payload;
case UNSET_ACTIVE_EVENT:
return null;
default:
return state;
}
};

18
app/reducers/auctions.js Normal file
View File

@@ -0,0 +1,18 @@
import { Map } from 'immutable';
import { AUCTIONS_UPDATED, UPDATE_AUCTIONS } from '../constants/actionTypes.js';
export const auctions = (state = new Map(), action) => {
switch (action.type) {
case AUCTIONS_UPDATED:
return state.merge(
action.payload.toMap().mapEntries((entry) => {
const [, item] = entry;
return [`${item.id}`, item];
}),
);
case UPDATE_AUCTIONS:
default:
return state;
}
};

12
app/reducers/blockUI.js Normal file
View File

@@ -0,0 +1,12 @@
import { BLOCK_UI, UNBLOCK_UI } from '../constants/actionTypes.js';
export const blockUI = (state = false, action) => {
switch (action.type) {
case BLOCK_UI:
return true;
case UNBLOCK_UI:
return false;
default:
return state;
}
};

View File

@@ -1,16 +1,17 @@
import { Map } from 'immutable';
import { EVENTS_LOADED, GET_EVENTS } from '../constants/actionTypes.js';
export const events = (state = {}, action) => {
export const events = (state = new Map(), action) => {
switch (action.type) {
case GET_EVENTS:
return Object.assign({}, state, {
isFetching: true,
});
case EVENTS_LOADED:
return Object.assign({}, state, {
events: action.payload,
isFetching: false,
});
return state.merge(
action.payload.toMap().mapEntries((entry) => {
const [, event] = entry;
return [`${event.id}`, event];
}),
);
case GET_EVENTS:
default:
return state;
}

View File

@@ -1,17 +1,15 @@
import { combineReducers } from 'redux';
import { combineReducers } from 'redux-immutable';
import { activeEvent } from './activeEvent.js';
import { auctions } from './auctions.js';
import { blockUI } from './blockUI.js';
import { events } from './events.js';
import { items } from './items.js';
const initialState = {
auction: {},
cart: [],
isFetching: false,
items: [],
profile: {},
};
const rootReducer = combineReducers({
activeEvent,
auctions,
blockUI,
events,
items,
});

View File

@@ -1,16 +1,19 @@
import { ITEMS_LOADED, GET_ITEMS } from '../constants/actionTypes.js';
import { Map } from 'immutable';
export const items = (state = {}, action) => {
import {
GET_ITEMS,
ITEMS_LOADED,
} from '../constants/actionTypes.js';
export const items = (state = new Map(), action) => {
switch (action.type) {
case GET_ITEMS:
return Object.assign({}, state, {
isFetching: true,
});
case ITEMS_LOADED:
return Object.assign({}, state, {
items: action.payload,
isFetching: false,
const mapped = action.payload.toMap().mapEntries((entry) => {
const [, item] = entry;
return [`${item.id}`, item];
});
return state.merge(mapped);
case GET_ITEMS:
default:
return state;
}

17
app/reducers/profile.js Normal file
View File

@@ -0,0 +1,17 @@
import { Map } from 'immutable';
import {
SET_PROFILE,
UPDATE_PROFILE,
} from '../constants/actionTypes.js';
export const profile = (state = new Map(), action) => {
switch (action.type) {
case SET_PROFILE:
return action.payload;
case UPDATE_PROFILE:
return action.payload;
default:
return state;
}
};

View File

@@ -3,7 +3,7 @@ import { Dimensions, Platform } from 'react-native';
import { createBottomTabNavigator, createStackNavigator } from 'react-navigation';
import { Icon } from 'react-native-elements';
import Auction from './screens/Auction.js';
import Auction from './containers/Auction.js';
import Checkout from './screens/Checkout.js';
import Event from './screens/Event.js';
import Events from './screens/Events.js';
@@ -19,28 +19,28 @@ export const Tabs = createBottomTabNavigator({
screen: Event,
navigationOptions: {
tabBarLabel: 'Event',
tabBarIcon: ({ tintColor }) => <Icon name="black-tie" type="fontawesome" size={28} color={tintColor} />,
tabBarIcon: ({ tintColor }) => <Icon name="black-tie" type="font-awesome" size={28} color={tintColor} />,
},
},
'Auction': {
screen: Auction,
navigationOptions: {
tabBarLabel: 'Silent Auction',
tabBarIcon: ({ tintColor }) => <Icon name="gavel" type="fontawesome" size={28} color={tintColor} />,
tabBarIcon: ({ tintColor }) => <Icon name="gavel" type="font-awesome" size={28} color={tintColor} />,
},
},
'Bazaar': {
screen: Marketplace,
navigationOptions: {
tabBarLabel: 'Bazaar',
tabBarIcon: ({ tintColor }) => <Icon name="shopping-store" type="fontisto" size={28} color={tintColor} />,
tabBarIcon: ({ tintColor }) => <Icon name="store" type="fontisto" size={28} color={tintColor} />,
},
},
'Profile': {
screen: Profile,
navigationOptions: {
tabBarLabel: 'Profile',
tabBarIcon: ({ tintColor }) => <Icon name="ios-person-outline" type="ionicon" size={28} color={tintColor} />,
tabBarIcon: ({ tintColor }) => <Icon name="ios-person" type="font-awesome" size={28} color={tintColor} />,
},
},
});

View File

@@ -10,6 +10,7 @@ import {
import { SORT_MODES, VIEW_MODES } from '../constants/constants.js';
import FilterBar from '../components/Auction/FilterBar.js';
import GridItem from '../components/Item/Grid.js';
import ListItem from '../components/Item/List.js';
@@ -17,14 +18,16 @@ export default class Auction extends Component {
static get propTypes() {
return {
changeFilter: PropTypes.func,
items: PropTypes.array.isRequired,
fetchItems: PropTypes.func.isRequired,
fetchStatus: PropTypes.func.isRequired,
items: PropTypes.array,
};
}
static get defaultProps() {
return {
changeFilter: () => { console.log('Change Filter Default Prop', arguments); },
header: null,
items: [],
};
}
@@ -40,6 +43,11 @@ export default class Auction extends Component {
};
}
componentDidMount() {
this.props.fetchStatus();
this.props.fetchItems();
}
changeFilter(filter) {
this.props.changeFilter('auction', filter);
}
@@ -48,18 +56,20 @@ export default class Auction extends Component {
this.setState({ view: mode });
}
_keyExtractor = (item, index) => item.id;
_keyExtractor = (item, index) => `${item._id}_${index}`;
_renderItem = (view) => ({ item }) => {
console.log('_renderItem', item);
if (view === VIEW_MODES.GRID) {
return <GridItem details={item} />;
return <GridItem {...item} />;
}
return <ListItem details={item} />;
return <ListItem {...item} />;
}
render() {
const { items, view } = this.state;
const { items } = this.props;
const { sort, view } = this.state;
return (
<View style={styles.container}>
@@ -67,11 +77,15 @@ export default class Auction extends Component {
changeFilterer={this.changeFilter}
changeViewMode={this.changeViewMode}
/>
<FlatList
data={items}
keyExtractor={this._keyExtractor}
renderItem={this.renderItem(view)}
/>
{items.size > 0 && (
<FlatList
data={items}
keyExtractor={this._keyExtractor}
renderItem={this._renderItem(view)}
contentContainerStyle={styles.itemListContentContainer}
style={styles.itemList}
/>
)}
</View>
);
}
@@ -84,9 +98,11 @@ const styles = StyleSheet.create({
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
title: {
fontSize: 20,
textAlign: 'center',
margin: 10,
}
itemList: {
width: '100%',
},
itemListContentContainer: {
alignItems: 'stretch',
justifyContent: 'flex-start',
},
});

View File

@@ -1,4 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
Text,
@@ -6,6 +8,21 @@ import {
} from 'react-native';
export default class Events extends Component {
static get propTypes() {
return {
events: PropTypes.array.isRequired,
fetchEvents: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
}
componentDidMount() {
}
render() {
return (
<View style={styles.container}>

13
app/selectors/auctions.js Normal file
View File

@@ -0,0 +1,13 @@
import { createSelector } from 'reselect';
//import { getItemsIdsWithNoBids, getMyBidItemIds, getMyWinningItemIds } from './auctions.js';
const getState = (state) => state;
export const getItemBidCount = (state, itemId) => state.getIn(['auctions', itemId, 'bidCount'], 0);
export const getItemPrice = (state, itemId) => state.getIn(['auctions', itemId, 'currentPrice'], 0);
export const isBiddingItem = (state, itemId) => state.getIn(['auctions', itemId, 'isBidding'], false);
export const isWinningItem = (state, itemId) => state.getIn(['auctions', itemId, 'isWinning'], false);

42
app/selectors/items.js Normal file
View File

@@ -0,0 +1,42 @@
import { createSelector } from 'reselect';
//import { getItemsIdsWithNoBids, getMyBidItemIds, getMyWinningItemIds } from './auctions.js';
const getState = (state) => state;
export const getItem = (state, itemId) => state.getIn(['items', itemId], false);
export const getItems = createSelector(
[getState],
(state) => state.get('items') || new Map(),
);
export const getItemsAsList = createSelector(
[getItems],
(itemsAsMap) => itemsAsMap.toList(),
);
export const getAuctionItems = createSelector(
[getState],
(state) => state.get('items').filter(i => i.type === 'auction') || new Map(),
);
export const getAuctionItemsAsList = createSelector(
[getAuctionItems],
(auctionItemsAsMap) => auctionItemsAsMap.toList(),
);
export const getTicketItems = createSelector(
[getState],
(state) => state.get('items').filter(i => i.type === 'ticket') || new Map(),
);
export const getTicketItemsAsList = createSelector(
[getTicketItems],
(ticketItemsAsMap) => ticketItemsAsMap.toList(),
);
export const getAuctionItemsWithNoBids = createSelector(
[getAuctionItems],
(auctionItemsAsMap) => auctionItemsAsMap.filter(i => i.bidCount),
);

View File

@@ -1,9 +1,14 @@
import { Map } from 'immutable';
import { applyMiddleware, compose, createStore } from 'redux';
import { composeWithDevTools } from 'remote-redux-devtools';
import thunk from 'redux-thunk';
import rootReducer from '../reducers/index.js';
const composeEnhancers = composeWithDevTools({ port: 8000, realtime: true, suppressConnectErrors: false });
export const store = createStore(
rootReducer,
compose(applyMiddleware(thunk)),
Map(),
composeEnhancers(applyMiddleware(thunk)),
);