mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-29 05:00:43 +10:00
InboxPage with order and sale transactions
This commit is contained in:
parent
42cd32f7e4
commit
d58b87ba72
8 changed files with 312 additions and 36 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
96
src/containers/InboxPage/InboxPage.duck.js
Normal file
96
src/containers/InboxPage/InboxPage.duck.js
Normal 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;
|
||||
});
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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} />);
|
||||
|
|
|
|||
|
|
@ -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))>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue