Pen Settings

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URL's added here will be added as <link>s in order, and before the CSS in the editor. If you link to another Pen, it will include the CSS from that Pen. If the preprocessor matches, it will attempt to combine them before processing.

+ add another resource

You're using npm packages, so we've auto-selected Babel for you here, which we require to process imports and make it all work. If you need to use a different JavaScript preprocessor, remove the packages in the npm tab.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Use npm Packages

We can make npm packages available for you to use in your JavaScript. We use webpack to prepare them and make them available to import. We'll also process your JavaScript with Babel.

⚠️ This feature can only be used by logged in users.

Code Indentation

     

Save Automatically?

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

HTML Settings

Here you can Sed posuere consectetur est at lobortis. Donec ullamcorper nulla non metus auctor fringilla. Maecenas sed diam eget risus varius blandit sit amet non magna. Donec id elit non mi porta gravida at eget metus. Praesent commodo cursus magna, vel scelerisque nisl consectetur et.

            
              // VARIABLES
// --> colors
// --> grids
// --> typography
// 
// BASE
// --> lists
// --> media
// --> page
// --> typography
// 
// COMPONENTS
// --> button
// --> cart
// --> header
// --> item
// --> nomatch
// --> select
// --> shop
// 
// STYLES
// --> main

@import "breakpoint";


// VARIABLES
// colors
$fontColor: #333;
$linkColor: #555;
$linkActiveColor: #27a;

$itemSoldOutColor: #f00;

$headerBorderColor: #999;
$headerBgColor: rgba(255, 255, 255, 0.85);
$headerFallbackBgColor: #fff;

$cartButtonBgColor: rgba(102, 136, 255, 0.85);
$cartButtonFallbackBgColor: #68f;
$cartButtonBorderColor: #46f;
$cartButtonHoverBgColor: rgba(85, 119, 255, 0.85);
$cartButtonHoverFallbackBgColor: #57f;
$cartButtonHoverBorderColor: #35f;
$cartButtonActiveBgColor: rgba(51, 85, 255, 0.85);
$cartButtonActiveFallbackBgColor: #35f;
$cartButtonActiveBorderColor: #24f;

$selectBgColor: #fff;
$selectBorderColor: #666;

$buttonFontColor: #fff;
$buttonBgColor: #2d2;
$buttonBorderColor: #2b2;

// grids
$smallTabletBP: 40rem;
$largeTabletBP: 54rem;
$maxWidth: 75rem;

$headerHeight: 4.375rem;

$itemBP: 48rem;
$itemMobileMaxWidth: 25rem;
$itemDesktopMaxWidth: 60rem;

$cartMaxWidth: 60rem;

$shopItemMaxWidth: 21.625rem;

$buttonMaxWidth: 20rem;

// typography
$fontFamily: Helvetica, Arial, sans-serif;

$rootFontSize: 16px;

$mainHeaderFontSize: 1.75rem;

$shopItemNameFontSize: 1.5rem;
$shopItemPriceFontSize: 1rem;

$cartItemNameFontSize: 1.25rem;
$cartItemPriceFontSize: 1rem;
$cartItemDeleteFontSize: 1.5rem;

$itemNameFontSize: 1.5rem;
$itemPriceFontSize: 1.25rem;

$totalFontSize: 1.25rem;

$qtySelectFontSize: 1rem;

$buttonFontSize: 1.25rem;


// BASE
// lists
ul {
  padding: 0;
  margin: 0;
  list-style: none;
}

// media
img, media {
  max-width: 100%;
}

// page
* {
  box-sizing: border-box;
}

body {
  margin: 0;
}

// typography
html {
  font-size: $rootFontSize;
}

body {
  font-family: $fontFamily;
  color: $fontColor;
}

a {
  text-decoration: none;
  transition: 0.2s;
  color: $linkColor;
  &:hover, &:active, &:focus {
    color: $linkActiveColor;
  }
}

h1, h2 {
  margin: 0;
}


// COMPONENTS
// button
.item-add-button, .cart-pay-button {
  font-size: $buttonFontSize;
  font-weight: bold;
  color: $buttonFontColor;
  background-color: $buttonBgColor;
  display: block;
  width: 100%;
  max-width: $buttonMaxWidth;
  padding: 0.25rem 0.5rem;
  margin: 1.5rem auto 0;
  border: none;
  border-radius: 0.375rem;
  box-shadow: 0 0.375rem $buttonBorderColor;
  transition: 0.2s;
  &:active {
    transform: translateY(0.375rem);
    box-shadow: 0 0 $buttonBorderColor;
  }
}

// cart
.empty-cart, .cart-items {
  margin: 2rem 0;
}

.empty-cart {
  text-align: center;
}

.cart-item {
  display: flex;
  align-items: center;
  margin: 1rem 0;
}

.cart-item-image-link, .cart-item-delete {
  flex: 0 0 auto;
}

.cart-item-info {
  flex: 1 1 auto;
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
}

.cart-item-delete {
  font-size: $cartItemDeleteFontSize;
  margin: 0.5rem;
}

.cart-item-image {
  vertical-align: bottom;
  height: 5rem;
  padding: 0 0.8333rem 0 0.8333rem;
}

.cart-item-name-link, .cart-item-value {
  flex: 0 0 auto;
  margin: 0.25rem 0.5rem;
}

.cart-item-name {
  font-size: $cartItemNameFontSize;
}

.cart-item-value {
  font-size: $cartItemPriceFontSize;
}

.cart-item-price {
  margin-right: 1rem;
}

.cart-item-qty-select {
  margin-left: 0.5rem;
}

.cart-total {
  font-size: $totalFontSize;
  display: flex;
  justify-content: space-between;
}

.cart-total-label {
  font-weight: bold;
}

// header
.header {
  width: 100%;
  height: $headerHeight;
  position: fixed;
  top: 0;
  left: 0;
  background-color: $headerFallbackBgColor;
  background-color: $headerBgColor;
  border-bottom: 0.0625rem solid $headerBorderColor;
}

.header-contents {
  max-width: $maxWidth;
  margin: 0 auto;
  display: flex;
  & > a {
    flex: 0 0 auto;
  }
}

.back-button {
  margin: 1rem auto 1rem 1rem;
  padding: 0.625rem 0;
}

.cart-button {
  margin: 1rem 1rem 1rem auto;
  padding: 0.5rem;
  border: 0.125rem solid $cartButtonBorderColor;
  border-radius: 0.375rem;
  background-color: $cartButtonFallbackBgColor;
  background-color: $cartButtonBgColor;
  color: $buttonFontColor;
  font-weight: bold;
  &:hover, &:active, &:focus {
    color: $buttonFontColor;
  }
  &:hover, &:focus {
    border: 0.125rem solid $cartButtonHoverBorderColor;
    background-color: $cartButtonHoverFallbackBgColor;
    background-color: $cartButtonHoverBgColor;
  }
  &:active {
    border: 0.125rem solid $cartButtonActiveBorderColor;
    background-color: $cartButtonActiveFallbackBgColor;
    background-color: $cartButtonActiveBgColor;
  }
}

// item
.item {
  max-width: $itemMobileMaxWidth;
  margin: 0 auto;
}

.item-name, .item-price {
  padding: 0.25rem 0;
}

.item-name {
  font-size: $itemNameFontSize;
}

.item-price {
  font-size: $itemPriceFontSize;
  font-weight: normal;
}

.item-add-form, .item-sold-out {
  margin: 2.5rem 0 0;
}

.item-add-form {
  text-align: center;
}

.item-qty-label {
  margin-right: 0.5rem;
}

.item-sold-out {
  color: $itemSoldOutColor;
}

@include breakpoint($itemBP) {
  .item {
    display: flex;
    max-width: $itemDesktopMaxWidth;
    padding: 2rem 0 0;
  }

  .item-image {
    flex: 0 0 auto;
  }

  .item-details {
    flex: 1 1 auto;
    padding: 2rem 0 1rem 2rem;
  }
}

// nomatch
.no-match {
  text-align: center;
  margin: 2rem 0;
}

// select
.item-qty, .cart-item-qty-select {
  font-size: $qtySelectFontSize;
  color: $fontColor;
  width: 2.75rem;
  background-color: $selectBgColor;
  border: 0.0625rem solid $selectBorderColor;
  border-radius: 0.25rem;
}

// shop
.shop-item {
  margin: 2rem auto;
  max-width: $shopItemMaxWidth;
  border: 0.0625rem solid $linkColor;
  box-shadow: 0 0 0.625rem 0.25rem $linkColor;
}

.shop-item-image, .shop-item-name, .shop-item-price {
  padding: 0.5rem 0.75rem;
}

.shop-item-image {
  margin: 0;
}

.shop-item-name {
  font-size: $shopItemNameFontSize;
}

.shop-item-price {
  font-size: $shopItemPriceFontSize;
  font-weight: normal;
  padding-bottom: 1rem;
}

@include breakpoint($smallTabletBP) {
  .shop-item-list {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
    justify-content: flex-start;
  }

  .shop-item {
    flex: 0 0 43%;
    margin: 2rem 3.5%;
  }
}

@include breakpoint($largeTabletBP) {
  .shop-item {
    flex: 0 0 29%;
    margin: 2rem 2.166%;
  }
}


// STYLES
// main
.shopping-cart-app {
  max-width: $maxWidth;
  margin: 0 auto;
}

.main-header {
  font-size: $mainHeaderFontSize;
  margin: 1rem 0;
}

.main {
  padding: $headerHeight 1rem 2.5rem;
}

            
          
!
            
              // INITIAL STATE
// --------------------
// --> initialState
// 
// FUNCTIONS
// --------------------
// --> getOptionsArray
// 
// ACTIONS
// --------------------
// --> addToCart
// --> removeFromCart
// --> updateCartItem
// --> removeStockItem
// 
// REDUCERS
// --------------------
// ------> cartItem
// ----> cart
// ------> stockItem
// ----> stock
// --> reducers
// 
// COMPONENTS
// --------------------
// ------> Header
// ----> HeaderContainer
// ----------> ShopItem
// --------> ShopItems
// ------> ShopItemsContainer
// ----> Shop
// ----------> AddItem
// --------> AddItemContainer
// ------> Item
// ----> ItemContainer
// ----------> CartItem
// --------> CartItems
// --------> Total
// --------> PayButton
// ------> Cart
// ----> CartContainer
// ----> NoMatch
// --> App

const { PropTypes } = React;
const { connect, Provider } = ReactRedux;
const {
  Router,
  Route,
  Link,
  IndexRoute,
  hashHistory
} = ReactRouter;

// INITIAL STATE
const initialState = {
  cart: [],
  stock: [
    {
      id: 0,
      name: 'TC 2017 LS',
      description: 'VC FlexLight Jersey with spot sublimated Team Canada 2017 logo, from our Team Canada Collection.',
      price: 34.95,
      count: 12,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/TC2017_LS_Mens_grande.jpg?v=1485541617',
    }, {
      id: 1,
      name: 'TC 2017 Shorts',
      description: 'VC FlexLight Shorts with spot sublimated Team Canada 2017 logo, from our Team Canada Collection.',
      price: 25.00,
      count: 7,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/TC2017_Shorts_White_grande.jpg?v=1485541580',
    }, {
      id: 2,
      name: 'TC 2016 Red SS',
      description: 'VC Ultimate\'s fully sublimated performance jersey, a replica of one of the two official dark jerseys of 2016 Team Canada teams! Sublimated jerseys are made with VC\'s FlexLight performance material – soft, lightweight and moisture wicking.',
      price: 74.00,
      count: 11,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/TC2016_red_SS_front_grande.jpg?v=1468602448',
    }, {
      id: 3,
      name: 'TC 2016 Dark SS',
      description: 'VC Ultimate\'s fully sublimated performance jersey, a replica of one of the two official dark jerseys of 2016 Team Canada teams! Sublimated jerseys are made with VC\'s FlexLight performance material – soft, lightweight and moisture wicking.',
      price: 35.99,
      count: 13,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/TC2016_SS_front_grande.jpg?v=1460557226',
    }, {
      id: 4,
      name: 'TC 2016 Light SS',
      description: 'Official replica of the 2016 Team Canada light playing jersey. VC spot sublimated jerseys are made with VC\'s FlexLight performance material – soft, lightweight and moisture wicking.',
      price: 35.99,
      count: 16,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/TCsample2_white_front_grande.jpg?v=1480111096',
    }, {
      id: 5,
      name: 'Goat Shorts',
      description: 'This just in... VC\'s NEW signature \'GOAT\' style athletic shorts now with pockets! Made with our FlexLight material.',
      price: 29.00,
      count: 4,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/MedGreyPockets_front_grande400x600_grande_888f95c1-0f4d-483f-938e-c686892a855a_grande.jpg?v=1454966549',
    }, {
      id: 6,
      name: 'Friction Gloves',
      description: 'This is the glove that started it all! Friction Gloves work great in every condition: dry, hot, rain, snow, you name it. They will help you maintain a firm grip on the disc whether you\'re catching or throwing. They eliminate slippage when the disc is wet, keep your hands warm in cold weather, and ease the sting of zippy throws. Whether your goal is to throw with more accuracy, catch tough discs, or just look fly, Frictions will help.  Frictions are not bulky or stiff like other gloves. They are tight fitting and, after a while, you\'ll forget they\'re even on!',
      price: 33.95,
      count: 18,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/Gloves_grande.jpg?v=1446586810',
    }, {
      id: 7,
      name: 'TC 2017 Trucker',
      description: 'Premium meshback cotton front trucker hat. Made by FlexFit, with the official embroidered patch of Team Canada 2017! 47% Cotton / 25% Polyester / 28% Nylon, contrast trucker mesh back, matching plastic snap, hard buckram, matching undervisor. One size fits all',
      price: 15.00,
      count: 15,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/TC2017_Hat_Snapback_BlackWhite_grande.jpg?v=1485538793',
    }, {
      id: 8,
      name: 'VC Discraft Disc',
      description: 'Discraft 175 gram Ultrastar disc, hot stamped with the original VC logo.',
      price: 14.00,
      count: 19,
      img: 'https://cdn.shopify.com/s/files/1/0340/2849/products/Disc_VC_OG_grande_grande_cc0c1695-4247-426a-bd6c-5821da4e42da_grande.png?v=1485469652',
    },
  ],
};


// FUNCTIONS
// getOptionsArray
const getOptionsArray = (count) => {
  const array = [];
  for (let i = 0; i < count; i++) {
    array.push(i + 1);
  }

  return array;
};

// ACTIONS
// addToCart
const addToCart = (id, count) => (
  {
    type: 'ADD_TO_CART',
    id,
    count,
  }
);

// removeFromCart
const removeFromCart = (id) => (
  {
    type: 'REMOVE_FROM_CART',
    id,
  }
);

// updateCartItem
const updateCartItem = (id, count) => (
  {
    type: 'UPDATE_CART_ITEM',
    id,
    count,
  }
);

// removeStockItem
const removeStockItem = (id, count) => (
  {
    type: 'REMOVE_STOCK_ITEM',
    id,
    count,
  }
);


// REDUCERS
// cartItem
const cartItem = (state, action) => {
  switch (action.type) {
    case 'ADD_TO_CART':
      return {
        id: action.id,
        count: action.count,
      };
    case 'REMOVE_FROM_CART':
      return state.id !== action.id;
    case 'UPDATE_CART_ITEM':
      if (state.id !== action.id) {
        return state;
      }

      return Object.assign(
        {},
        state,
        {
          count: action.count,
        }
      );
    default:
      return state;
  }
};

// cart
const cart = (state = [], action) => {
  switch (action.type) {
    case 'ADD_TO_CART':
      return [
        ...state,
        cartItem(undefined, action),
      ];
    case 'REMOVE_FROM_CART':
      return state.filter(item => cartItem(item, action));
    case 'UPDATE_CART_ITEM':
      return state.map(item => cartItem(item, action));
    default:
      return state;
  }
};

// stockItem
const stockItem = (state, action) => {
  switch (action.type) {
    case 'REMOVE_STOCK_ITEM':
      if (state.id !== action.id) {
        return state;
      }

      return Object.assign(
        {},
        state,
        {
          count: state.count - action.count,
        }
      );
    default:
      return state;
  }
};

// stock
const stock = (state = [], action) => {
  switch (action.type) {
    case 'REMOVE_STOCK_ITEM':
      return state.map(item => stockItem(item, action));
    default:
      return state;
  }
};

// reducers
const reducers = Redux.combineReducers({
  cart,
  stock,
});


// COMPONENTS
// Header
const Header = ({ children, cartItems, backButton, cartButton }) => {
  const getBackButton = () => (
    <Link to='/' className='back-button'>
      &lt; Back to shop
    </Link>
  );

  const getCartButton = () => (
    <Link to='/cart' className='cart-button'>
      Cart ({cartItems})
    </Link>
  );

  return (
    <div className='shopping-cart-app'>
      <header className='header'>
        <div className='header-contents'>
          {backButton ? getBackButton() : ''}
          {cartButton ? getCartButton() : ''}
        </div>
      </header>
      <main className='main'>
        {children}
      </main>
    </div>
  );
};

Header.PropTypes = {
  cartItems: PropTypes.number.isRequired,
  backButton: PropTypes.bool.isRequired,
  cartButton: PropTypes.bool.isRequired,
};


// HeaderContainer
const showBackButton = (pathname) => (
  pathname !== '/' ? true : false
);

const showCartButton = (pathname) => (
  !pathname.includes('cart') ? true : false
);

const HeaderContainer = connect(
  (state, ownProps) => (
    {
      children: ownProps.children,
      cartItems: state.cart.length,
      backButton: showBackButton(ownProps.location.pathname),
    cartButton: showCartButton(ownProps.location.pathname),
    }
  )
)(Header);


// ShopItem
const ShopItem = ({ id, name, price, img }) => (
  <li className={'shop-item shop-item-' + id}>
    <Link to={'/item/' + id}>
      <div className='shop-item-container'>
        <img
          className='shop-item-image'
          src={img}
        />
        <h1 className='shop-item-name'>
          {name}
        </h1>
        <h2 className='shop-item-price'>
          ${price.toFixed(2)}
        </h2>
      </div>
    </Link>
  </li>
);

ShopItem.PropTypes = {
  id: PropTypes.number.isRequired,
  name: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  img: PropTypes.string.isRequired,
};


// ShopItems
const ShopItems = ({ items }) => {
  if (!items.length) {
    return <p className='no-shop-items'>No items</p>;
  }

  return (
    <ul className='shop-item-list'>
      {items.map(item =>
        <ShopItem
          key={item.id}
          id={item.id}
          name={item.name}
          price={item.price}
          img={item.img}
        />
      )}
    </ul>
  );
};

ShopItems.PropTypes = {
  items: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    description: PropTypes.string.isRequired,
    price: PropTypes.number.isRequired,
    count: PropTypes.number.isRequired,
  }).isRequired).isRequired,
};


// ShopItemsContainer
const ShopItemsContainer = connect(
  (state) => (
    {
      items: state.stock,
    }
  )
)(ShopItems);


// Shop
const Shop = () => (
  <div className='shop'>
    <h1 className='main-header shop-header'>Shop</h1>
    <ShopItemsContainer />
  </div>
);


// AddItem
const AddItem = ({ id, count, onSubmit }) => {
  if (!count) {
    return (
      <p className='item-sold-out'>
        Sold out!
      </p>
    );
  }

  return (
    <form
      className='item-add-form'
      onSubmit={e => {
        e.preventDefault();
        onSubmit(e, id);
      }}
    >
      <span className='item-qty-label'>
        Qty:
      </span>
      <select className='item-qty'>
        {getOptionsArray(count).map(num =>
          <option
            key={num}
            value={num}
          >
            {num}
          </option>
        )}
      </select>
      <button
        className='item-add-button'
        type='submit'
      >
        Add to cart
      </button>
    </form>
  );
};

AddItem.PropTypes = {
  count: PropTypes.number.isRequired,
  onClick: PropTypes.func.isRequired,
};


// AddItemContainer
const addItemGetSelectedValue = (e) => (
  e.target.getElementsByClassName('item-qty')[0].value
);

const AddItemContainer = connect(
  (state, ownProps) => (
    {
      id: ownProps.id,
      count: state.stock.find(item => item.id === ownProps.id).count,
      inCart: state.cart.some(item => item.id === ownProps.id),
    }
  ),
  null,
  (stateProps, dispatchProps, ownProps) => {
    const onSubmit = stateProps.inCart ? updateCartItem : addToCart;

    return Object.assign({}, ownProps, stateProps, dispatchProps, {
      onSubmit: (e, id) => {
        dispatchProps.dispatch(onSubmit(id, addItemGetSelectedValue(e)));
      },
    });
  }
)(AddItem);


// Item
const Item = ({ id, name, description, price, img }) => (
  <div className={'item item-' + id}>
    <img
      className='item-image'
      src={img}
    />
    <div className='item-details'>
      <h1 className='item-name'>
        {name}
      </h1>
      <h2 className='item-price'>
        ${price.toFixed(2)}
      </h2>
      <p className='item-desc'>
        {description}
      </p>
      <AddItemContainer id={id} />
    </div>
  </div>
);

Item.PropTypes = {
  id: PropTypes.number.isRequired,
  name: PropTypes.string.isRequired,
  description: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  img: PropTypes.string.isRequired,
};


// ItemContainer
const ItemContainer = connect(
  (state, ownProps) => (
    state.stock.find(item => String(item.id) === ownProps.params.id)
  )
)(Item);


// CartItem
const CartItem = (
  { id, name, price, img, count, stockCount, onQtyChange, onRemoveClick }
) => (
  <li className={'cart-item cart-item-' + id}>
    <Link
      to={'/item/' + id}
      className='cart-item-image-link'
    >
      <img
        className='cart-item-image'
        src={img}
      />
    </Link>
    <div className='cart-item-info'>
      <Link
        to={'/item/' + id}
        className='cart-item-name-link'
      >
        <h1 className='cart-item-name'>
          {name}
        </h1>
      </Link>
      <div className='cart-item-value'>
        <span className='cart-item-price'>
          ${price.toFixed(2)}
        </span>
        <span className='cart-item-qty'>
          Qty:
          <select
            className='cart-item-qty-select'
            value={count}
            onChange={(e) => onQtyChange(e, id)}
          >
            {getOptionsArray(stockCount).map(num =>
              <option
                key={num}
                value={num}
              >
                {num}
              </option>
            )}
          </select>
        </span>
      </div>
    </div>
    <a
      href="#"
      className='cart-item-delete'
      onClick={(e) => {
        onRemoveClick(e, id);
      }}
    >
      ×
    </a>
  </li>
);

CartItem.PropTypes = {
  id: PropTypes.number.isRequired,
  name: PropTypes.string.isRequired,
  price: PropTypes.number.isRequired,
  img: PropTypes.string.isRequired,
  count: PropTypes.number.isRequired,
  stockCount: PropTypes.number.isRequired,
  onQtyChange: PropTypes.func.isRequired,
  onRemoveClick: PropTypes.func.isRequired,
};


// CartItems
const CartItems = ({ cart, onQtyChange, onRemoveClick }) => {
  if (!cart.length) {
    return <p className='empty-cart'>Cart is empty</p>;
  }

  return (
    <ul className='cart-items'>
      {cart.map(item =>
        <CartItem
          key={item.id}
          id={item.id}
          name={item.name}
          price={item.price}
          img={item.img}
          count={item.count}
          stockCount={item.stockCount}
          onQtyChange={(e, id) => onQtyChange(e, id)}
          onRemoveClick={(e, id) => onRemoveClick(e, id)}
        />
      )}
    </ul>
  );
};

CartItems.PropTypes = {
  cart: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    price: PropTypes.number.isRequired,
    img: PropTypes.string.isRequired,
    count: PropTypes.number.isRequired,
    stockCount: PropTypes.number.isRequired,
  }).isRequired).isRequired,
  onQtyChange: PropTypes.func.isRequired,
  onRemoveClick: PropTypes.func.isRequired,
};


// Total
const Total = ({ cart }) => (
  <div className='cart-total'>
    <span className='cart-total-label'>
      Total:
    </span>
    <span className='cart-total-value'>
      ${cart.length ? cart.reduce((acc, item) => (
        acc + item.price * item.count
      ), 0).toFixed(2) : Number(0).toFixed(2)}
    </span>
  </div>
);

Total.PropTypes = {
  cart: PropTypes.arrayOf(PropTypes.shape({
    price: PropTypes.number.isRequired,
    count: PropTypes.number.isRequired,
  }).isRequired).isRequired,
};


// PayButton
const PayButton = ({ onPayClick }) => (
  <button
    type='button'
    className='cart-pay-button'
    onClick={() => onPayClick()}
  >
    Pay now
  </button>
);

PayButton.PropTypes = {
  onPayClick: PropTypes.func.isRequired,
};


// Cart
const Cart = ({ cart, onQtyChange, onRemoveClick, onPayClick }) => (
  <div className='cart'>
    <h1 className='main-header cart-header'>My Cart</h1>
    <CartItems
      cart={cart}
      onQtyChange={onQtyChange}
      onRemoveClick={onRemoveClick}
    />
    <Total cart={cart} />
    <PayButton onPayClick={onPayClick} />
  </div>
);

Cart.PropTypes = {
  cart: PropTypes.arrayOf(PropTypes.shape({
    id: PropTypes.number.isRequired,
    name: PropTypes.string.isRequired,
    price: PropTypes.number.isRequired,
    img: PropTypes.string.isRequired,
    count: PropTypes.number.isRequired,
    stockCount: PropTypes.number.isRequired,
  }).isRequired).isRequired,
  onQtyChange: PropTypes.func.isRequired,
  onRemoveClick: PropTypes.func.isRequired,
  onPayClick: PropTypes.func.isRequired,
};


// CartContainer
const cartGetSelectedValue = (e) => (
  e.target.value
);

const CartContainer = connect(
  (state) => (
    {
      cart: state.cart.map(cartItem => {
        const item = state.stock.find(stockItem => cartItem.id === stockItem.id);
        return {
          id: cartItem.id,
          name: item.name,
          price: item.price,
          img: item.img,
          count: cartItem.count,
          stockCount: item.count,
        };
      }),
    }
  ),
  (dispatch) => (
    {
      onQtyChange: (e, id) => {
        dispatch(updateCartItem(id, cartGetSelectedValue(e)));
      },

      onRemoveClick: (e, id) => {
        e.preventDefault();
        dispatch(removeFromCart(id));
      },

      dispatch: (reducer) => dispatch(reducer),
    }
  ),
  (stateProps, dispatchProps, ownProps) => (
    Object.assign({}, ownProps, stateProps, dispatchProps, {
      onPayClick: () =>
        stateProps.cart.map(item => {
          dispatchProps.dispatch(removeStockItem(item.id, item.count));
          dispatchProps.dispatch(removeFromCart(item.id));
        }),
    })
  )
)(Cart);


// NoMatch
const NoMatch = () => (
  <p className='no-match'>
    Sorry, the page you are looking for does not exist.
    Click above to go back to the shop.
  </p>
);


// App
const App = () => (
  <Router history={hashHistory}>
    <Route path='/' component={HeaderContainer}>
      <IndexRoute component={Shop} />
      <Route path='item/:id' component={ItemContainer} />
      <Route path='cart' component={CartContainer} />
      <Route path='*' component={NoMatch} />
    </Route>
  </Router>
);

const store = Redux.createStore(reducers, initialState);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('app')
);

            
          
!
999px
🕑 One or more of the npm packages you are using needs to be built. You're the first person to ever need it! We're building it right now and your preview will start updating again when it's ready.

Console