diff --git a/src/containers/InboxPage/InboxPage.css b/src/containers/InboxPage/InboxPage.css
index 5aa94ed1..4ab2be16 100644
--- a/src/containers/InboxPage/InboxPage.css
+++ b/src/containers/InboxPage/InboxPage.css
@@ -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;
+}
diff --git a/src/containers/InboxPage/InboxPage.duck.js b/src/containers/InboxPage/InboxPage.duck.js
new file mode 100644
index 00000000..75c68886
--- /dev/null
+++ b/src/containers/InboxPage/InboxPage.duck.js
@@ -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;
+ });
+ };
diff --git a/src/containers/InboxPage/InboxPage.js b/src/containers/InboxPage/InboxPage.js
index 75606155..458a2ef1 100644
--- a/src/containers/InboxPage/InboxPage.js
+++ b/src/containers/InboxPage/InboxPage.js
@@ -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 (
+
+
+

+
+
+
+ {otherUserName}
+ {timestamp(intl, tx)}
+
+
{txState(intl, tx)}
+
+
+ );
+};
+
+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 ;
+ }
+
+ 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 (
+
+
+
+ );
+ };
return (
@@ -17,26 +103,53 @@ export const InboxPageComponent = props => {
+
+ {!fetchInProgress ? transactions.map(toTxItem) : null}
+
);
};
-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;
diff --git a/src/containers/InboxPage/InboxPage.test.js b/src/containers/InboxPage/InboxPage.test.js
index ab40c7c8..e2eff6e9 100644
--- a/src/containers/InboxPage/InboxPage.test.js
+++ b/src/containers/InboxPage/InboxPage.test.js
@@ -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();
diff --git a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
index 8347e444..b0f6d351 100644
--- a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
+++ b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
@@ -9,17 +9,29 @@ exports[`InboxPage matches snapshot 1`] = `
+
`;
diff --git a/src/containers/reducers.js b/src/containers/reducers.js
index b8650974..768f9afb 100644
--- a/src/containers/reducers.js
+++ b/src/containers/reducers.js
@@ -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 };
diff --git a/src/routesConfiguration.js b/src/routesConfiguration.js
index 2f3b3530..b227aaee 100644
--- a/src/routesConfiguration.js
+++ b/src/routesConfiguration.js
@@ -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: () => ,
+ },
+ {
+ path: '/inbox/:tab',
+ auth: true,
+ exact: true,
name: 'InboxPage',
- component: () => ,
- },
- {
- path: '/inbox/orders',
- auth: true,
- exact: true,
- name: 'InboxOrdersPage',
- component: props => ,
- },
- {
- path: '/inbox/sales',
- auth: true,
- exact: true,
- name: 'InboxSalesPage',
- component: props => ,
+ component: props => ,
+ loadData: (params, search) => InboxPage.loadData(params, search),
},
{
path: '/order/:id',
@@ -281,6 +276,12 @@ const routesConfiguration = [
name: 'StyleguideComponentExampleRaw',
component: props => ,
},
+ {
+ path: '/notfound',
+ exact: true,
+ name: 'NotFoundPage',
+ component: props => ,
+ },
];
export default routesConfiguration;
diff --git a/src/translations/en.json b/src/translations/en.json
index c34bbcaa..b5204ffe 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -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.",