InboxPage with order and sale transactions

This commit is contained in:
Kimmo Puputti 2017-04-12 16:04:20 +03:00
parent 42cd32f7e4
commit d58b87ba72
8 changed files with 312 additions and 36 deletions

View file

@ -14,3 +14,48 @@
font-weight: bold;
text-decoration: underline;
}
.listItem {
border-bottom: 1px solid #979797;
&:last-child {
border-bottom: none;
}
}
.itemLink {
display: flex;
padding: 1rem;
text-decoration: none;
}
.itemAvatar {
margin-right: 1rem;
width: 44px;
height: 44px;
}
.itemAvatarImage {
border-radius: 50%;
}
.itemInfo {
flex: 1;
}
.itemUsername {
font-weight: bold;
font-size: 16px;
text-decoration: none;
}
.itemTimestamp {
float: right;
font-size: 14px;
}
.itemState {
margin-top: 5px;
font-size: 14px;
text-decoration: none;
}

View file

@ -0,0 +1,96 @@
import _ from 'lodash';
import { addEntities } from '../../ducks/sdk.duck';
// ================ Action types ================ //
export const FETCH_ORDERS_OR_SALES_REQUEST = 'app/InboxPage/FETCH_ORDERS_OR_SALES_REQUEST';
export const FETCH_ORDERS_OR_SALES_SUCCESS = 'app/InboxPage/FETCH_ORDERS_OR_SALES_SUCCESS';
export const FETCH_ORDERS_OR_SALES_ERROR = 'app/InboxPage/FETCH_ORDERS_OR_SALES_ERROR';
// ================ Reducer ================ //
const entityRefs = entities =>
entities.map(entity => ({
id: entity.id,
type: entity.type,
}));
const initialState = {
fetchInProgress: false,
fetchOrdersOrSalesError: null,
transactionRefs: [],
};
export default function checkoutPageReducer(state = initialState, action = {}) {
const { type, payload } = action;
switch (type) {
case FETCH_ORDERS_OR_SALES_REQUEST:
return { ...state, fetchInProgress: true, fetchOrdersOrSalesError: null };
case FETCH_ORDERS_OR_SALES_SUCCESS:
return { ...state, fetchInProgress: false, transactionRefs: entityRefs(payload) };
case FETCH_ORDERS_OR_SALES_ERROR:
console.error(payload); // eslint-disable-line
return { ...state, fetchInProgress: false, fetchOrdersOrSalesError: payload };
default:
return state;
}
}
// ================ Action creators ================ //
const fetchOrdersOrSalesRequest = () => ({ type: FETCH_ORDERS_OR_SALES_REQUEST });
const fetchOrdersOrSalesSuccess = transactions => ({
type: FETCH_ORDERS_OR_SALES_SUCCESS,
payload: transactions,
});
const fetchOrdersOrSalesError = e => ({
type: FETCH_ORDERS_OR_SALES_ERROR,
error: true,
payload: e,
});
// ================ Thunks ================ //
const sortedTransactions = txs =>
_.chain(txs)
.sortBy(tx => {
return tx.attributes ? tx.attributes.lastTransitionedAt : null;
})
.reverse()
.value();
export const loadData = params =>
(dispatch, getState, sdk) => {
const { tab } = params;
const onlyFilterValues = {
orders: 'order',
sales: 'sale',
};
const onlyFilter = onlyFilterValues[tab];
if (!onlyFilter) {
return Promise.reject(new Error(`Invalid tab for InboxPage: ${tab}`));
}
dispatch(fetchOrdersOrSalesRequest());
const queryParams = {
only: onlyFilter,
include: ['provider', 'customer'],
};
return sdk.transactions
.query(queryParams)
.then(response => {
const transactions = sortedTransactions(response.data.data);
dispatch(addEntities(response));
dispatch(fetchOrdersOrSalesSuccess(transactions));
return response;
})
.catch(e => {
dispatch(fetchOrdersOrSalesError(e));
throw e;
});
};

View file

@ -1,15 +1,101 @@
import React, { PropTypes } from 'react';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { PageLayout, NamedLink, H1 } from '../../components';
import { PageLayout, NamedRedirect, NamedLink, H1 } from '../../components';
import * as propTypes from '../../util/propTypes';
import { getEntities } from '../../ducks/sdk.duck';
import { loadData } from './InboxPage.duck';
import css from './InboxPage.css';
const { shape, string, arrayOf, bool, oneOf } = PropTypes;
// Formatted username
const username = user => {
const profile = user && user.attributes && user.attributes.profile
? user.attributes.profile
: null;
return profile ? `${profile.firstName} ${profile.lastName}` : '';
};
// Localised timestamp when the given transaction was updated
const timestamp = (intl, tx) => {
const date = tx.attributes ? tx.attributes.lastTransitionedAt : null;
return date ? intl.formatDate(date) : '';
};
// Translated name of the state of the given transaction
const txState = (intl, tx) => {
const { state } = tx;
if (state === propTypes.TX_STATE_ACCEPTED) {
return intl.formatMessage({
id: 'InboxPage.stateAccepted',
});
} else if (state === propTypes.TX_STATE_REJECTED) {
return intl.formatMessage({
id: 'InboxPage.stateRejected',
});
}
return intl.formatMessage({
id: 'InboxPage.statePending',
});
};
const InboxItem = props => {
const { type, tx, intl } = props;
const { customer, provider } = tx;
const isOrder = type === 'order';
const otherUserName = username(isOrder ? provider : customer);
const otherUserAvatar = 'https://placehold.it/44x44';
return (
<NamedLink
className={css.itemLink}
name={isOrder ? 'OrderDetailsPage' : 'SaleDetailsPage'}
params={{ id: tx.id.uuid }}
>
<div className={css.itemAvatar}>
<img className={css.itemAvatarImage} src={otherUserAvatar} alt={otherUserName} />
</div>
<div className={css.itemInfo}>
<div>
<span className={css.itemUsername}>{otherUserName}</span>
<span className={css.itemTimestamp}>{timestamp(intl, tx)}</span>
</div>
<div className={css.itemState}>{txState(intl, tx)}</div>
</div>
</NamedLink>
);
};
InboxItem.propTypes = {
type: oneOf(['order', 'sale']).isRequired,
tx: propTypes.transaction.isRequired,
intl: intlShape.isRequired,
};
export const InboxPageComponent = props => {
const { tab, intl } = props;
const { fetchInProgress, transactions, intl, params } = props;
const { tab } = params;
const validTab = tab === 'orders' || tab === 'sales';
if (!validTab) {
return <NamedRedirect name="NotFoundPage" />;
}
const isOrders = tab === 'orders';
const ordersTitle = intl.formatMessage({ id: 'InboxPage.ordersTitle' });
const salesTitle = intl.formatMessage({ id: 'InboxPage.salesTitle' });
const title = tab === 'orders' ? ordersTitle : salesTitle;
const title = isOrders ? ordersTitle : salesTitle;
const toTxItem = tx => {
return (
<li key={tx.id.uuid} className={css.listItem}>
<InboxItem type={isOrders ? 'order' : 'sale'} tx={tx} intl={intl} />
</li>
);
};
return (
<PageLayout title={title}>
@ -17,26 +103,53 @@ export const InboxPageComponent = props => {
<FormattedMessage id="InboxPage.title" />
</H1>
<nav>
<NamedLink className={css.tab} name="InboxOrdersPage" activeClassName={css.activeTab}>
<NamedLink
className={css.tab}
name="InboxPage"
params={{ tab: 'orders' }}
activeClassName={tab === 'orders' ? css.activeTab : null}
>
<FormattedMessage id="InboxPage.ordersTabTitle" />
</NamedLink>
<NamedLink className={css.tab} name="InboxSalesPage" activeClassName={css.activeTab}>
<NamedLink
className={css.tab}
name="InboxPage"
params={{ tab: 'sales' }}
activeClassName={tab === 'sales' ? css.activeTab : null}
>
<FormattedMessage id="InboxPage.salesTabTitle" />
</NamedLink>
</nav>
<ul>
{!fetchInProgress ? transactions.map(toTxItem) : null}
</ul>
</PageLayout>
);
};
const { oneOf } = PropTypes;
InboxPageComponent.propTypes = {
tab: oneOf(['orders', 'sales']).isRequired,
params: shape({
tab: string.isRequired,
}).isRequired,
fetchInProgress: bool.isRequired,
transactions: arrayOf(propTypes.transaction).isRequired,
// from injectIntl
intl: intlShape.isRequired,
};
const InboxPage = injectIntl(InboxPageComponent);
const mapStateToProps = state => {
const marketplaceData = state.data;
const refs = state.InboxPage.transactionRefs;
return {
fetchInProgress: state.InboxPage.fetchInProgress,
transactions: getEntities(marketplaceData, refs),
};
};
const InboxPage = compose(connect(mapStateToProps), injectIntl)(InboxPageComponent);
InboxPage.loadData = loadData;
export default InboxPage;

View file

@ -6,7 +6,11 @@ import { InboxPageComponent } from './InboxPage';
describe('InboxPage', () => {
it('matches snapshot', () => {
const props = {
tab: 'orders',
params: {
tab: 'orders',
},
fetchInProgress: false,
transactions: [],
intl: fakeIntl,
};
const tree = renderShallow(<InboxPageComponent {...props} />);

View file

@ -9,17 +9,29 @@ exports[`InboxPage matches snapshot 1`] = `
</H1>
<nav>
<withFlattenedRoutes(withRouter(NamedLink))
name="InboxOrdersPage">
name="InboxPage"
params={
Object {
"tab": "orders",
}
}>
<FormattedMessage
id="InboxPage.ordersTabTitle"
values={Object {}} />
</withFlattenedRoutes(withRouter(NamedLink))>
<withFlattenedRoutes(withRouter(NamedLink))
name="InboxSalesPage">
activeClassName={null}
name="InboxPage"
params={
Object {
"tab": "sales",
}
}>
<FormattedMessage
id="InboxPage.salesTabTitle"
values={Object {}} />
</withFlattenedRoutes(withRouter(NamedLink))>
</nav>
<ul />
</Connect(withRouter(PageLayout))>
`;

View file

@ -5,7 +5,8 @@
*/
import CheckoutPage from './CheckoutPage/CheckoutPage.duck';
import EditListingPage from './EditListingPage/EditListingPage.duck';
import InboxPage from './InboxPage/InboxPage.duck';
import ListingPage from './ListingPage/ListingPage.duck';
import SearchPage from './SearchPage/SearchPage.duck';
export { CheckoutPage, EditListingPage, ListingPage, SearchPage };
export { CheckoutPage, EditListingPage, InboxPage, ListingPage, SearchPage };

View file

@ -10,6 +10,7 @@ import {
LandingPage,
ListingPage,
ManageListingsPage,
NotFoundPage,
OrderPage,
PasswordChangePage,
PasswordForgottenPage,
@ -157,22 +158,16 @@ const routesConfiguration = [
path: '/inbox',
auth: true,
exact: true,
name: 'InboxBasePage',
component: () => <NamedRedirect name="InboxPage" params={{ tab: 'orders' }} />,
},
{
path: '/inbox/:tab',
auth: true,
exact: true,
name: 'InboxPage',
component: () => <NamedRedirect name="InboxOrdersPage" />,
},
{
path: '/inbox/orders',
auth: true,
exact: true,
name: 'InboxOrdersPage',
component: props => <InboxPage {...props} tab="orders" />,
},
{
path: '/inbox/sales',
auth: true,
exact: true,
name: 'InboxSalesPage',
component: props => <InboxPage {...props} tab="sales" />,
component: props => <InboxPage {...props} />,
loadData: (params, search) => InboxPage.loadData(params, search),
},
{
path: '/order/:id',
@ -281,6 +276,12 @@ const routesConfiguration = [
name: 'StyleguideComponentExampleRaw',
component: props => <StyleguidePage {...props} />,
},
{
path: '/notfound',
exact: true,
name: 'NotFoundPage',
component: props => <NotFoundPage {...props} />,
},
];
export default routesConfiguration;

View file

@ -6,22 +6,23 @@
"BookingDatesForm.bookingStartTitle": "Start date",
"BookingDatesForm.placeholder": "mm/dd/yyyy",
"BookingDatesForm.priceRequired": "Oops, this listing has no price!",
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.requestToBook": "Request to book",
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.requiredDate": "Oops, make sure your date is correct!",
"BookingDatesForm.youWontBeChargedInfo": "You won't be charged yet",
"BookingInfo.bookingPeriodLabel": "Booking period:",
"BookingInfo.bookingPeriod": "{bookingStart} - {bookingEnd}",
"BookingInfo.bookingPeriodLabel": "Booking period:",
"BookingInfo.nightCount": "{count, number} {count, plural, one {day} other {days}}",
"BookingInfo.pricePerDay":"Price per day:",
"BookingInfo.total": "Total:",
"CheckoutPage.title": "Book {listingTitle}",
"CheckoutPage.initiateOrderError": "Payment request failed. Please try again.",
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",
"CheckoutPage.paymentTitle": "Payment",
"CheckoutPage.title": "Book {listingTitle}",
"DateInput.clearDate": "Clear Date",
"DateInput.closeDatePicker": "Close",
"DateInput.defaultPlaceholder": "Date input",
"DateInput.screenReaderInputMessage": "Date input",
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",
"EditListingForm.bankAccountNumberRequired": "You need to add a bank account number.",
"EditListingForm.descriptionRequired": "You need to add a description.",
"EditListingForm.imageRequired": "You need to add at least one image.",
@ -37,15 +38,18 @@
"HeroSearchForm.search": "Search",
"HeroSection.subTitle": "The largest online community to rent music studios",
"HeroSection.title": "Book Studiotime anywhere",
"InboxPage.title": "Inbox",
"InboxPage.ordersTitle": "Inbox: Orders",
"InboxPage.salesTitle": "Inbox: Sales",
"InboxPage.ordersTabTitle": "Guest",
"InboxPage.ordersTitle": "Inbox: Orders",
"InboxPage.salesTabTitle": "Host",
"ModalInMobile.closeModal": "Close modal",
"InboxPage.salesTitle": "Inbox: Sales",
"InboxPage.stateAccepted": "Accepted",
"InboxPage.statePending": "Pending",
"InboxPage.stateRejected": "Rejected",
"InboxPage.title": "Inbox",
"ListingPage.ctaButtonMessage": "Book {title}",
"ListingPage.loadingListingData": "Loading listing data",
"ListingPage.noListingData": "Could not find listing data",
"ModalInMobile.closeModal": "Close modal",
"PageLayout.authInfoFailed": "Could not get authentication information.",
"PageLayout.logoutFailed": "Logout failed. Please try again.",
"SearchPage.foundResults": "{count, number} {count, plural, one {listing} other {listings}} found.",