Skip to content

Instantly share code, notes, and snippets.

@SariSaar
Last active March 4, 2024 16:05
Show Gist options
  • Save SariSaar/3fc9f88986526b537f5512ba0d8b6414 to your computer and use it in GitHub Desktop.
Save SariSaar/3fc9f88986526b537f5512ba0d8b6414 to your computer and use it in GitHub Desktop.
Code examples for building a shopping cart in Sharetribe Web Template – Viewing cart items
import React from 'react';
import { AddToCartButton, NamedLink, ResponsiveImage } from '../../../components';
import { createSlug } from '../../../util/urlHelpers';
import css from '../CartCard/CartCard.module.css';
const CartCard = props => {
const { listing, count, onToggleCart } = props;
const { title, price } = listing.attributes;
const listingId = listing.id.uuid;
const authorId = listing.author.id.uuid;
const variantPrefix = 'cart-card';
const handleToggleCart = increment => {
onToggleCart(listingId, authorId, increment);
};
const linkParams = { id: listingId, slug: createSlug(title) };
const firstImage = listing.images && listing.images.length > 0 ? listing.images[0] : null;
const variants = firstImage
? Object.keys(firstImage?.attributes?.variants).filter(k => k.startsWith(variantPrefix))
: [];
const total = `${(price.amount * count) / 100} ${price.currency}`;
return (
<div className={css.cardLayout}>
<NamedLink name="ListingPage" params={linkParams}>
{title}
</NamedLink>
<div className={css.itemLayout}>
<ResponsiveImage
rootClassName={css.rootForImage}
alt={title}
image={firstImage}
variants={variants}
/>
<AddToCartButton listing={listing} count={count} incrementCart={handleToggleCart} />
<span>{total}</span>
</div>
</div>
);
};
CartCard.defaultProps = {
listing: null,
};
export default CartCard;
.rootForImage {
/* Layout - image will take space defined by aspect ratio wrapper */
width: 100%;
border: solid 1px var(--matterColorNegative);
border-radius: 4px;
height: 80px;
width: 80px;
}
.cardLayout {
display: flex;
flex-direction: column;
padding-left: 45px;
}
.itemLayout {
display: flex;
flex-direction: row;
}
import React from 'react';
import { Form as FinalForm } from 'react-final-form';
import { FieldSelect, Form } from '../../components';
export const CartDeliveryForm = props => (
<FinalForm
{...props}
render={formRenderProps => {
const { intl, handleSubmit, className } = formRenderProps;
return (
<Form onChange={handleSubmit}>
<FieldSelect
id="delivery"
name="delivery"
className={className}
label={intl.formatMessage({ id: 'CartPage.selectDeliveryMethod' })}
>
<option value="" disabled>
{intl.formatMessage({ id: 'CartPage.optionSelect' })}
</option>
<option value="shipping">
{intl.formatMessage({ id: 'CartPage.optionShipping' })}
</option>
<option value="pickup">{intl.formatMessage({ id: 'CartPage.optionPickup' })}</option>
</FieldSelect>
</Form>
);
}}
/>
);
import {
updatedEntities,
denormalisedEntities,
denormalisedResponseEntities,
} from '../../util/data';
import { storableError } from '../../util/errors';
import { createImageVariantConfig } from '../../util/sdkLoader';
import { parse } from '../../util/urlHelpers';
// import { cartTransactionLineItems } from '../../util/api';
import { currentUserShowSuccess, fetchCurrentUser } from '../../ducks/user.duck';
export const FETCH_LISTINGS_REQUEST = 'app/CartPage/FETCH_LISTINGS_REQUEST';
export const FETCH_LISTINGS_SUCCESS = 'app/CartPage/FETCH_LISTINGS_SUCCESS';
export const FETCH_LISTINGS_ERROR = 'app/CartPage/FETCH_LISTINGS_ERROR';
export const FETCH_LINE_ITEMS_REQUEST = 'app/CartPage/FETCH_LINE_ITEMS_REQUEST';
export const FETCH_LINE_ITEMS_SUCCESS = 'app/CartPage/FETCH_LINE_ITEMS_SUCCESS';
export const FETCH_LINE_ITEMS_ERROR = 'app/CartPage/FETCH_LINE_ITEMS_ERROR';
export const TOGGLE_CART_REQUEST = 'app/CartPage/TOGGLE_CART_REQUEST';
export const TOGGLE_CART_SUCCESS = 'app/CartPage/TOGGLE_CART_SUCCESS';
export const TOGGLE_CART_ERROR = 'app/CartPage/TOGGLE_CART_ERROR';
export const TOGGLE_DELIVERY_REQUEST = 'app/CartPage/TOGGLE_DELIVERY_REQUEST';
export const TOGGLE_DELIVERY_SUCCESS = 'app/CartPage/TOGGLE_DELIVERY_SUCCESS';
export const TOGGLE_DELIVERY_ERROR = 'app/CartPage/TOGGLE_DELIVERY_ERROR';
export const ADD_CART_ENTITIES = 'app/CartPage/ADD_CART_ENTITIES';
export const SET_CURRENT_AUTHOR = 'app/CartPage/SET_CURRENT_AUTHOR';
export const SET_CURRENT_AUTHOR_DELIVERY = 'app/CartPage/SET_CURRENT_AUTHOR_DELIVERY';
export const SET_AUTHOR_IDX = 'app/CartPage/SET_AUTHOR_IDX';
export const deliveryOptions = {
BOTH: 'both',
SHIPPING: 'shipping',
PICKUP: 'pickup',
NONE: 'none',
};
const RESULT_PAGE_SIZE = 8;
// ================ Reducer ================ //
const initialState = {
authorIdx: 0,
cart: {},
cartEntities: {},
cartLineItems: [],
currentPageResultIds: [],
currentAuthor: null,
currentAuthorDelivery: null,
pagination: null,
queryParams: null,
queryInProgress: false,
queryListingsError: null,
lineItemsInProgress: false,
lineItemsError: null,
toggleCartInProgress: false,
toggleCartError: null,
toggleDeliveryInProgress: false,
toggleDeliveryError: null,
};
const resultIds = data => data.data.map(l => l.id);
const merge = (state, sdkResponse) => {
const apiResponse = sdkResponse.data;
return {
...state,
cartEntities: updatedEntities({ ...state.cartEntities }, apiResponse),
};
};
const cartPageReducer = (state = initialState, action = {}) => {
const { type, payload } = action;
switch (type) {
case FETCH_LISTINGS_REQUEST:
return {
...state,
queryParams: payload.queryParams,
queryInProgress: true,
queryListingsError: null,
currentPageResultIds: [],
};
case FETCH_LISTINGS_SUCCESS:
return {
...state,
currentPageResultIds: resultIds(payload.data),
pagination: payload.data.meta,
queryInProgress: false,
cart: payload.cart,
};
case FETCH_LISTINGS_ERROR:
// eslint-disable-next-line no-console
console.error(payload);
return { ...state, queryInProgress: false, queryListingsError: payload };
case FETCH_LINE_ITEMS_REQUEST:
return { ...state, lineItemsInProgress: true, lineItemsError: null };
case FETCH_LINE_ITEMS_SUCCESS:
return { ...state, lineItemsInProgress: false, cartLineItems: payload };
case FETCH_LINE_ITEMS_ERROR:
return { ...state, lineItemsInProgress: false, lineItemsError: payload };
case TOGGLE_CART_REQUEST:
return { ...state, toggleCartInProgress: true, toggleCartError: null };
case TOGGLE_CART_SUCCESS:
return { ...state, toggleCartInProgress: false, cart: payload };
case TOGGLE_CART_ERROR:
return { ...state, toggleCartInProgress: false, toggleCartError: payload };
case TOGGLE_DELIVERY_REQUEST:
return { ...state, toggleDeliveryInProgress: true, toggleDeliveryError: null };
case TOGGLE_DELIVERY_SUCCESS:
return {
...state,
toggleDeliveryInProgress: false,
cart: payload.cart,
currentAuthorDelivery: payload.delivery,
};
case TOGGLE_DELIVERY_ERROR:
return { ...state, toggleDeliveryInProgress: false, toggleDeliveryError: payload };
case ADD_CART_ENTITIES:
return merge(state, payload);
case SET_CURRENT_AUTHOR:
return { ...state, currentAuthor: payload, currentAuthorDelivery: null };
case SET_CURRENT_AUTHOR_DELIVERY:
return { ...state, currentAuthorDelivery: payload };
case SET_AUTHOR_IDX:
return { ...state, authorIdx: payload };
default:
return state;
}
};
export default cartPageReducer;
// ================ Selectors ================ //
/**
* Get the denormalised cart listing entities with the given IDs
*
* @param {Object} state the full Redux store
* @param {Array<UUID>} listingIds listing IDs to select from the store
*/
export const getCartListingsById = (state, listingIds) => {
const { cartEntities } = state.CartPage;
const resources = listingIds.map(id => ({
id,
type: 'listing',
}));
const throwIfNotFound = false;
return denormalisedEntities(cartEntities, resources, throwIfNotFound);
};
/**
* Return the listing ids of an author specific cart
* @param {*} cart
* @returns array of listing ids
*/
export const getCartListingIds = cart => {
return Object.keys(cart).filter(key => key !== 'deliveryMethod');
}
/**
* Get the total number of items in cart. Optionally get the count for only
* the author being currently viewed on CartPage
* @param {*} state
* @param {*} useCurrentAuthorOnly
*/
export const getCartCount = (state, useCurrentAuthorOnly = false) => {
const { cart } = state.user?.currentUser?.attributes.profile.privateData || {};
const { currentAuthor } = state?.CartPage;
const authorId =
useCurrentAuthorOnly && cart ? currentAuthor?.id.uuid ?? Object.keys(cart)[0] : null;
if (!cart || (useCurrentAuthorOnly && !authorId)) {
return null;
}
let counts;
if (authorId) {
counts = getAuthorListingIds(authorId, cart).map(l => cart[authorId][l].count);
} else {
counts = Object.keys(cart).flatMap(author => {
const listings = getAuthorListingIds(author, cart);
return listings.map(l => cart[author][l].count);
});
}
return counts.length ? counts.reduce((acc, val) => acc + val) : 0;
};
// ================ Action creators ================ //
export const addCartEntities = sdkResponse => ({
type: ADD_CART_ENTITIES,
payload: sdkResponse,
});
export const queryListingsRequest = queryParams => ({
type: FETCH_LISTINGS_REQUEST,
payload: { queryParams },
});
export const queryListingsSuccess = (response, cart) => ({
type: FETCH_LISTINGS_SUCCESS,
payload: { data: response.data, cart },
});
export const queryListingsError = e => ({
type: FETCH_LISTINGS_ERROR,
error: true,
payload: e,
});
export const fetchLineItemsRequest = () => ({ type: FETCH_LINE_ITEMS_REQUEST });
export const fetchLineItemsSuccess = result => ({
type: FETCH_LINE_ITEMS_SUCCESS,
payload: result.data,
});
export const fetchLineItemsError = e => ({ type: FETCH_LINE_ITEMS_ERROR, error: true, payload: e });
export const toggleCartRequest = () => ({ type: TOGGLE_CART_REQUEST });
export const toggleCartSuccess = result => ({
type: TOGGLE_CART_SUCCESS,
payload: result,
});
export const toggleCartError = e => ({ type: TOGGLE_CART_ERROR, error: true, payload: e });
export const toggleDeliveryRequest = () => ({ type: TOGGLE_DELIVERY_REQUEST });
export const toggleDeliverySuccess = result => ({
type: TOGGLE_DELIVERY_SUCCESS,
payload: result,
});
export const toggleDeliveryError = e => ({ type: TOGGLE_DELIVERY_ERROR, error: true, payload: e });
export const setCurrentAuthor = author => ({ type: SET_CURRENT_AUTHOR, payload: author });
export const setCurrentAuthorDelivery = delivery => ({
type: SET_CURRENT_AUTHOR_DELIVERY,
payload: delivery,
});
export const setAuthorIdx = idx => ({ type: SET_AUTHOR_IDX, payload: idx });
/**
* Clear the cart related to the provider specified by authorId
* @param {*} authorId
*/
export const clearCart = authorId => (dispatch, getState, sdk) => {
dispatch(toggleCartRequest);
const { cart, currentAuthor } = getState().CartPage;
const newCart = {
...cart,
};
delete newCart[authorId];
dispatch(updateCurrentUserCart(newCart))
.then(() => {
dispatch(toggleCartSuccess(null));
if (currentAuthor?.id.uuid === authorId) {
dispatch(setCurrentAuthor(null));
}
})
.catch(e => {
dispatch(toggleCartError(storableError(e)));
});
};
/**
* Fetch listings currently in cart
* @param {*} queryParams
* @param {*} config
* @param {*} authorId
* @param {*} currentUser
*/
export const queryCartListings = (queryParams, config, authorId = null, currentUser = null) => (
dispatch,
getState,
sdk
) => {
dispatch(queryListingsRequest(queryParams));
const user = currentUser ?? getState().user.currentUser;
const cart = user?.attributes.profile.privateData?.cart || {};
const { currentAuthor } = getState().CartPage;
const cartAuthorId = authorId ?? currentAuthor?.id.uuid ?? Object.keys(cart)[0];
const { aspectWidth = 1, aspectHeight = 1 } = config.layout.listingImage;
const variantPrefix = 'cart-card';
const listingVariantPrefix = 'listing-card';
const aspectRatio = aspectHeight / aspectWidth;
const includeParams = {
perPage: RESULT_PAGE_SIZE,
include: ['images', 'author', 'currentStock'],
'fields.image': [
`variants.${variantPrefix}`,
`variants.${listingVariantPrefix}`,
`variants.${listingVariantPrefix}-2x`,
`variants.${listingVariantPrefix}-4x`,
`variants.${listingVariantPrefix}-6x`,
],
...createImageVariantConfig(`${variantPrefix}`, 100, aspectRatio),
...createImageVariantConfig(`${listingVariantPrefix}`, 400, aspectRatio),
...createImageVariantConfig(`${listingVariantPrefix}-2x`, 800, aspectRatio),
...createImageVariantConfig(`${listingVariantPrefix}-4x`, 1600, aspectRatio),
...createImageVariantConfig(`${listingVariantPrefix}-6x`, 2400, aspectRatio),
'limit.images': 1,
};
const { perPage, ...rest } = { ...queryParams, ...includeParams };
const ids = getAuthorListingIds(cartAuthorId, cart);
const params = {
...rest,
ids,
per_page: perPage,
};
return sdk.listings
.query(params)
.then(response => {
dispatch(addCartEntities(response));
dispatch(queryListingsSuccess(response, cart));
const author = response.data?.included?.filter(i => i.type === 'user')[0];
const newAuthorId = author?.id.uuid;
if (newAuthorId !== currentAuthor?.id.uuid) {
dispatch(setCurrentAuthor(author));
const authorDelivery = cart[newAuthorId].deliveryMethod;
dispatch(setCurrentAuthorDelivery(authorDelivery));
}
dispatch(getCartLineItems());
return response;
})
.catch(e => {
dispatch(queryListingsError(storableError(e)));
throw e;
});
};
/**
* Fetch line items for current author's cart
* @param {*} updatedCart
* @returns
*/
export const getCartLineItems = (updatedCart = null) => (dispatch, getState, sdk) => {
dispatch(fetchLineItemsRequest);
const { cart, currentAuthor } = getState().CartPage;
if (!currentAuthor || !cart || !Object.keys(cart).length) {
dispatch(fetchLineItemsSuccess({ data: [] }));
return;
}
const currentCart = updatedCart ?? cart;
if (currentAuthor) {
const authorCart = currentCart[(currentAuthor?.id?.uuid)];
cartTransactionLineItems({
isOwnListing: false,
orderData: {
cart: authorCart,
},
})
.then(resp => {
dispatch(fetchLineItemsSuccess(resp));
})
.catch(e => {
dispatch(fetchLineItemsError(storableError(e)));
});
}
};
// Add or remove items from cart
export const toggleCart = (listingId, authorId, increment = 1) => (dispatch, getState, sdk) => {
dispatch(toggleCartRequest);
const currentUser = getState().user.currentUser;
const cart = currentUser.attributes.profile.privateData?.cart || [];
// Cart as object with author ids as keys
let newCart = getNewCart(cart, authorId, listingId, increment);
dispatch(updateCurrentUserCart(newCart))
.then(updatedCart => {
dispatch(toggleCartSuccess(updatedCart));
// Only fetch listings if updated listing was removed from cart
if (!listingIsInCart(updatedCart, authorId, listingId)) {
dispatch(queryCartListings());
}
dispatch(getCartLineItems(updatedCart));
// If resulting cart is empty, clear current author
if (Object.keys(updatedCart).length === 0) {
dispatch(setCurrentAuthor(null));
}
})
.catch(e => {
dispatch(toggleCartError(storableError(e)));
});
};
/**
* Set selected delivery option for the specified author's cart
* @param {*} authorId
* @param {*} delivery
* @returns
*/
export const setCartDelivery = (authorId, delivery) => (dispatch, getState, sdk) => {
dispatch(toggleDeliveryRequest());
const currentUser = getState().user.currentUser;
const { currentAuthor } = getState().CartPage;
const cart = currentUser.attributes.profile.privateData?.cart || [];
const isCurrentAuthor = authorId === currentAuthor?.id?.uuid;
const isValidDelivery =
delivery === deliveryOptions.PICKUP || delivery === deliveryOptions.SHIPPING;
if (isValidDelivery && isCurrentAuthor) {
const newCart = {
...cart,
[authorId]: {
...cart[authorId],
deliveryMethod: delivery,
},
};
dispatch(updateCurrentUserCart(newCart))
.then(updatedCart => {
dispatch(toggleDeliverySuccess({ cart: updatedCart, delivery }));
dispatch(getCartLineItems())
})
.catch(e => {
console.log('e', e);
dispatch(toggleDeliveryError(storableError(e)));
});
}
};
/**
* Update the current user's cart to the provided newCart value
* @param {*} newCart
* @returns
*/
const updateCurrentUserCart = newCart => (dispatch, getState, sdk) => {
return sdk.currentUser
.updateProfile(
{
privateData: {
cart: newCart,
},
},
{ expand: true }
)
.then(resp => {
const entities = denormalisedResponseEntities(resp);
if (entities.length !== 1) {
throw new Error('Expected a resource in the sdk.currentUser.updateProfile response');
}
const currentUser = entities[0];
// Update current user in state.user.currentUser through user.duck.js
dispatch(currentUserShowSuccess(currentUser));
// Return the updated cart
return resp.data.data.attributes.profile.privateData.cart;
});
};
/**
* Get the listing ids for the listings in cart from the specified author
* @param {*} cartAuthorId
* @param {*} cart
* @returns array of listing id strings
*/
const getAuthorListingIds = (cartAuthorId, cart) => {
return (
(cartAuthorId &&
cart[cartAuthorId] &&
Object.keys(cart[cartAuthorId]).filter(key => key !== 'deliveryMethod')) ||
[]
);
};
/**
* Return true if the listing id is in the current user's cart, false otherwise
* @param {*} cart
* @param {*} authorId
* @param {*} listingId
* @returns boolean
*/
export const listingIsInCart = (cart, authorId, listingId) => {
if (!cart || !cart[authorId]) {
return false;
}
return Object.keys(cart[authorId]).includes(listingId);
};
/**
* Get the cart where the specified listing is incremented with the specified value.
* If the listing value increments to 0, remove listing. If the update removes the last
* listing for the author, remove author.
* @param {*} cart
* @param {*} authorId
* @param {*} listingId
* @param {*} increment
* @returns
*/
const getNewCart = (cart, authorId, listingId, increment) => {
const authorInCart = Object.keys(cart).includes(authorId);
const isListingInCart = listingIsInCart(cart, authorId, listingId);
const newCount = ((cart[authorId] && cart[authorId][listingId]?.count) || 0) + increment;
// Increment an existing listing
if (authorInCart && isListingInCart && newCount > 0) {
return {
...cart,
[authorId]: {
...cart[authorId],
[listingId]: {
count: newCount,
},
},
};
// Remove an existing listing from cart
} else if (authorInCart && isListingInCart && newCount <= 0) {
const newCart = { ...cart };
delete newCart[authorId][listingId];
const remainingCart = Object.keys(newCart[authorId]);
// If the listing was the author's last one, remove the author as well
if (
remainingCart.length == 0 ||
(remainingCart.length === 1 && remainingCart[0] === 'deliveryMethod')
) {
delete newCart[authorId];
}
return newCart;
// Add new listing to an existing author
} else if (authorInCart && !isListingInCart) {
return {
...cart,
[authorId]: {
...cart[authorId],
[listingId]: {
count: increment,
},
},
};
// Add new listing and a new author
} else {
return {
...cart,
[authorId]: {
[listingId]: {
count: increment,
},
},
};
}
};
export const loadData = (params, search, config, authorId = null) => dispatch => {
const queryParams = parse(search);
const page = queryParams.page || 1;
return Promise.all([dispatch(fetchCurrentUser())]).then(res => {
const currentUser = res[0];
return dispatch(
queryCartListings(
{
...queryParams,
page,
},
config,
authorId,
currentUser
)
);
});
};
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { useIntl } from 'react-intl';
import { isScrollingDisabled } from '../../ducks/ui.duck';
import { useConfiguration } from '../../context/configurationContext';
import { initializeCardPaymentData } from '../../ducks/stripe.duck.js';
import { createResourceLocatorString, findRouteByRouteName } from '../../util/routes';
import { useRouteConfiguration } from '../../context/routeConfigurationContext';
import { createSlug } from '../../util/urlHelpers';
import { PURCHASE_PROCESS_NAME } from '../../transactions/transaction.js';
import { FormattedMessage } from '../../util/reactIntl';
import { CartDeliveryForm } from './CartDeliveryForm.js';
import { Button, LayoutSingleColumn, Page, UserNav } from '../../components';
import EstimatedCustomerBreakdownMaybe from '../../components/OrderPanel/EstimatedCustomerBreakdownMaybe';
import CartCard from './CartCard/CartCard';
import css from './CartPage.module.css';
import TopbarContainer from '../../containers/TopbarContainer/TopbarContainer';
import FooterContainer from '../../containers/FooterContainer/FooterContainer';
import {
getCartListingsById,
loadData,
deliveryOptions,
setCartDelivery,
setAuthorIdx,
toggleCart,
} from './CartPage.duck';
const CartPage = props => {
const config = useConfiguration();
const routeConfiguration = useRouteConfiguration();
const intl = useIntl();
const {
currentAuthor,
listings,
cart,
cartLineItems = [],
callSetInitialValues,
scrollingDisabled,
currentAuthorDelivery: delivery,
toggleDeliveryInProgress,
queryInProgress,
setAuthorIdx,
authorIdx,
onInitializeCardPaymentData,
onReloadData,
onSetCartDelivery,
onToggleCart,
} = props;
const history = useHistory();
const authorId = currentAuthor?.id.uuid;
const authorName = currentAuthor?.attributes?.profile.displayName;
const authors = cart && Object.keys(cart);
// Determine whether the listings in the cart allow
// shipping, pickup, or both
const determineShipping = (acc, val) => {
const { shippingEnabled, pickupEnabled } = val.attributes.publicData;
return {
shippingEnabled: acc.shippingEnabled && shippingEnabled,
pickupEnabled: acc.pickupEnabled && pickupEnabled,
};
};
// Initialise the reducer with both options set to true
const cartDelivery = listings.reduce(determineShipping, {
shippingEnabled: true,
pickupEnabled: true,
});
const availableDelivery =
cartDelivery.shippingEnabled && cartDelivery.pickupEnabled
? deliveryOptions.BOTH
: cartDelivery.shippingEnabled
? deliveryOptions.SHIPPING
: cartDelivery.pickupEnabled
? deliveryOptions.PICKUP
: !!cartLineItems.length
? deliveryOptions.NONE
: null;
// Determine whether to automatically update cart delivery
// when only one delivery option is available for this author's cart
const updateDelivery =
(availableDelivery === deliveryOptions.PICKUP ||
availableDelivery === deliveryOptions.SHIPPING) &&
delivery !== availableDelivery &&
!toggleDeliveryInProgress &&
!queryInProgress &&
authorId &&
authorId === authors[authorIdx];
// Update cart delivery automatically if necessary
useEffect(() => {
if (updateDelivery && authorId) {
onSetCartDelivery(authorId, availableDelivery);
}
}, [updateDelivery, authorId, availableDelivery, onSetCartDelivery])
const submitSelectDelivery = values => {
const { delivery } = values;
onSetCartDelivery(authorId, delivery);
};
/**
* Render either a label for the delivery method or a form to select one
*/
const deliveryInfo = () => {
switch (availableDelivery) {
case deliveryOptions.BOTH:
return (
<CartDeliveryForm
onSubmit={submitSelectDelivery}
intl={intl}
className={css.deliverySelect}
initialValues={{ delivery }}
/>
);
case deliveryOptions.SHIPPING:
return <FormattedMessage id="CartPage.deliveryShipping" />;
case deliveryOptions.PICKUP:
return <FormattedMessage id="CartPage.deliveryPickup" />;
case deliveryOptions.NONE:
return <FormattedMessage id="CartPage.deliveryNotSet" />;
default:
return null;
}
};
const pageTitle = currentAuthor ? (
<FormattedMessage id="CartPage.pageTitleAuthor" values={{ name: authorName }} />
) : (
<FormattedMessage id="CartPage.pageTitleNoItems" />
);
/**
* Handle cart purchase:
* - Set necessary initial values
* - Redirect to CheckoutPage
*/
const buyCart = () => {
const listing = listings[0];
const saveToSessionStorage = props.currentUser;
// Customize checkout page state with current listing and selected orderData
const authorCart = cart[currentAuthor.id.uuid];
// Send details for each cart listing to CheckoutPage to be displayed
const cartListingDetails = listings.map(l => {
return {
id: l.id,
attributes: l.attributes,
price: `${l.attributes.price.amount / 100} ${l.attributes.price.currency}`,
count: authorCart[l.id.uuid].count,
images: l.images,
};
});
const initialValues = {
listing,
cartListings: cartListingDetails,
cartAuthorId: currentAuthor.id.uuid,
orderData: {
cart: authorCart,
deliveryMethod: delivery,
},
confirmPaymentError: null,
};
const { setInitialValues } = findRouteByRouteName('CheckoutPage', routeConfiguration);
callSetInitialValues(setInitialValues, initialValues, saveToSessionStorage);
// Clear previous Stripe errors from store if there is any
onInitializeCardPaymentData();
// Redirect to CheckoutPage
history.push(
createResourceLocatorString(
'CheckoutPage',
routeConfiguration,
{ id: listing.id.uuid, slug: createSlug(authorName) },
{}
)
);
};
/**
* Change the author whose cart is displayed
*/
const getNextAuthor = () => {
const newAuthorIdx = authorIdx + 1 < authors?.length ? authorIdx + 1 : 0;
setAuthorIdx(newAuthorIdx);
const authorId = authors[newAuthorIdx];
onReloadData(null, null, config, authorId);
};
/**
* Get the count of a specific item in the cart
*/
const getCartCount = listing => {
const listingId = listing?.id?.uuid;
const authorId = listing?.author?.id?.uuid;
return (
(authorId && listingId && cart && cart[authorId] && cart[authorId][listingId]?.count) || 0
);
};
return (
<Page title="Shopping cart" scrollingDisabled={scrollingDisabled}>
<LayoutSingleColumn
topbar={
<>
<TopbarContainer currentPage="CartPage" />
<UserNav currentPage="CartPage" />
</>
}
footer={<FooterContainer />}
>
<h1 className={css.title}>{pageTitle}</h1>
{authors?.length > 1 && (
<Button className={css.buyNowButton} onClick={getNextAuthor}>
<FormattedMessage id="CartPage.nextAuthor" count={authors.length} />
</Button>
)}
<div className={css.splitView}>
<div className={css.listingPanel}>
<div className={css.listingCards}>
{listings.map(l => (
<CartCard
key={l.id.uuid}
listing={l}
count={getCartCount(l)}
config={config}
onToggleCart={onToggleCart}
/>
))}
</div>
</div>
<div className={css.breakdownPanel}>
<EstimatedCustomerBreakdownMaybe
lineItems={cartLineItems}
processName={PURCHASE_PROCESS_NAME}
marketplaceName={config.marketplaceName}
/>
{deliveryInfo()}
</div>
</div>
{cartLineItems?.length > 0 && (
<Button className={css.buyNowButton} onClick={buyCart} disabled={!delivery}>
<FormattedMessage id="CartPage.buyNowButton" />
</Button>
)}
</LayoutSingleColumn>
</Page>
);
};
CartPage.defaultProps = {
listings: null,
};
const mapStateToProps = state => {
const {
authorIdx,
currentPageResultIds,
currentAuthor,
pagination,
queryInProgress,
queryListingsError,
queryParams,
cart,
cartLineItems,
currentAuthorDelivery,
toggleDeliveryInProgress,
} = state.CartPage;
const { currentUser } = state.user;
const listings = (currentPageResultIds && getCartListingsById(state, currentPageResultIds)) || [];
return {
authorIdx,
currentPageResultIds,
currentAuthor,
currentUser,
listings,
cart,
cartLineItems,
pagination,
queryInProgress,
queryListingsError,
queryParams,
scrollingDisabled: isScrollingDisabled(state),
currentAuthorDelivery,
toggleDeliveryInProgress,
};
};
const mapDispatchToProps = dispatch => ({
callSetInitialValues: (setInitialValues, initialValues, saveToSessionStorage) =>
dispatch(setInitialValues(initialValues, saveToSessionStorage)),
onInitializeCardPaymentData: () => dispatch(initializeCardPaymentData()),
onReloadData: (params, search, config, authorId) =>
dispatch(loadData(params, search, config, authorId)),
onSetCartDelivery: (authorId, delivery) => dispatch(setCartDelivery(authorId, delivery)),
onToggleCart: (listingId, authorId, increment) =>
dispatch(toggleCart(listingId, authorId, increment)),
setAuthorIdx: idx => dispatch(setAuthorIdx(idx)),
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(CartPage);
.title {
composes: h2 from global;
margin: 0 24px 24px 24px;
@media (--viewportMedium) {
margin: 0 24px 32px 24px;
}
@media (--viewportLarge) {
margin: 0 36px 32px 36px;
}
}
.messagePanel,
.listingPanel {
width: 100%;
margin: 24px auto 0 auto;
@media (--viewportMedium) {
margin: 48px auto 0 auto;
}
@media (--viewportLarge) {
margin: 72px auto 0 auto;
max-width: 1128px;
}
@media (--viewportXLarge) {
max-width: 1056px;
}
}
.listingCards {
padding: 24px;
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 24px;
width: 100%;
@media (min-width: 550px) {
grid-template-columns: repeat(1, 1fr);
}
@media (--viewportMedium) {
grid-template-columns: repeat(1, 1fr);
}
@media (--viewportLarge) {
grid-template-columns: repeat(1, 1fr);
padding: 0 36px;
}
}
.buyNowButton {
width: 150px;
float: right;
margin: 15px;
}
.splitView {
display: flex;
flex-direction: row;
}
.breakdownPanel {
margin: 45px;
width: 350px;
}
.deliverySelect {
margin-bottom: 24px;
@media (--viewportMedium) {
margin-bottom: 32px;
}
}
"CartPage.buyNowButton": "Buy now",
"CartPage.deliveryShipping": "Delivery method: shipping",
"CartPage.deliveryPickup": "Delivery method: pickup",
"CartPage.deliveryNotSet": "Delivery method not set. Please contact provider.",
"CartPage.nextAuthor": "Next",
"CartPage.optionSelect": "Select option...",
"CartPage.optionShipping": "Shipping",
"CartPage.optionPickup": "Pickup",
"CartPage.pageTitleAuthor": "Your cart from {name}",
"CartPage.pageTitleNoItems": "No items in cart",
"CartPage.selectDeliveryMethod": "Select your delivery method",
"TopbarDesktop.cart": "Cart",
"TopbarDesktop.yourCartLink": "Cart",
"TopbarMobileMenu.cartLink": "Cart",
"UserNav.cart": "Cart",
import {
sendInquiry,
setInitialValues,
fetchTimeSlots,
fetchTransactionLineItems,
// remove toggleCart from the import
} from './ListingPage.duck';
import { toggleCart } from '../CartPage/CartPage.duck.js';
...
import { loadData as CartPageLoader } from './CartPage/CartPage.duck';
...
const getPageDataLoadingAPI = () => {
return {
...
CartPage: {
loadData: CartPageLoader,
},
...
import CartPage from './CartPage/CartPage.duck';
...
export {
CartPage,
...
const CartPage = loadable(() => import(/* webpackChunkName: "CartPage" */ '../containers/CartPage/CartPage'));
...
{
path: '/cart',
name: 'CartPage',
auth: true,
authPage: 'LoginPage',
component: CartPage,
loadData: pageDataLoadingAPI.CartPage.loadData,
},
...
...
const CartLink = ({ notificationCount }) => {
const notificationDot = notificationCount > 0 ? <div className={css.notificationDot} /> : null;
return (
<NamedLink
className={css.topbarLink}
name="CartPage"
>
<span className={css.topbarLinkLabel}>
<FormattedMessage id="TopbarDesktop.cart" />
{notificationDot}
</span>
</NamedLink>
);
};
...
const ProfileMenu = ({ currentPage, currentUser, onLogout }) => {
...
return (
<Menu>
...
<MenuItem key="CartPage">
<NamedLink
className={classNames(css.menuLink, currentPageClass('CartPage'))}
name="CartPage"
>
<span className={css.menuItemBorder} />
<FormattedMessage id="TopbarDesktop.yourCartLink" />
</NamedLink>
</MenuItem>
...
</Menu>
);
};
const tabs = [
{
...
},
{
text: <FormattedMessage id="UserNav.cart" />,
selected: currentPage === 'CartPage',
linkProps: {
name: 'CartPage',
},
},
...
const TopbarDesktop = props => {
...
const { cart } = currentUser?.attributes.profile.privateData || {};
const cartCount = cart && Object.keys(cart).length || 0
const cartLinkMaybe = authenticatedOnClientSide ? (
<CartLink
notificationCount={cartCount}
/>
) : null;
return (
<nav className={classes}>
...
{inboxLinkMaybe}
{cartLinkMaybe}
{profileMenuMaybe}
...
</nav>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment