Repository: reactioncommerce/reaction-component-library
Pull request #89 (2018-06-19 03:59:48)

Add CartSummary Component
Adds a `CartSummary` component to renders a summary of the current items in the cart.


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e4cb7159..8d275f18 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -191,7 +191,7 @@ jobs:
               --name reactionapp_web_1 \
               -w /usr/local/src \
               "$DOCKER_REPOSITORY:$CIRCLE_SHA1" \
-              sh -c "cp reaction-app/package.json ./ && snyk test"
+              sh -c "cp reaction-app/package.json ./ && cp reaction-app/.snyk ./ && snyk test"
 
 workflows:
   version: 2
diff --git a/.snyk b/.snyk
index f0203238..d19842a0 100644
--- a/.snyk
+++ b/.snyk
@@ -236,4 +236,8 @@ ignore:
     - node-pre-gyp > request > stringstream:
         reason: None given
         expires: '2018-07-15T03:19:00.834Z'
+  'npm:ua-parser-js:20180227':
+    - '*':
+        reason: Waiting on fbjs to update to the latest ua-parser-js version
+        expires: 2018-06-30T00:00:00.000Z
 patch: {}
diff --git a/package/src/components/CartSummary/v1/CartSummary.js b/package/src/components/CartSummary/v1/CartSummary.js
new file mode 100644
index 00000000..6f87cd52
--- /dev/null
+++ b/package/src/components/CartSummary/v1/CartSummary.js
@@ -0,0 +1,144 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import styled from "styled-components";
+import { applyTheme } from "../../../utils";
+
+const Table = styled.table`
+  width: 100%;
+  background-color: ${applyTheme("color_black02")};
+  padding: 1rem;
+`;
+
+const Th = styled.th`
+  text-align: left;
+`;
+
+const Thr = styled.th`
+  text-align: right;
+  color: ${applyTheme("color_black30")};
+  font-weight: normal;
+`;
+
+const TdValue = styled.td`
+  text-align: right;
+  padding: 1rem 0;
+  color: ${applyTheme("color_coolGrey500")};
+`;
+
+const Td = styled.td`
+  padding: 1rem 0;
+  color: ${applyTheme("color_coolGrey400")};
+`;
+
+const Title = styled.span`
+  font-weight: ${applyTheme("font_weight_bold")};
+  color: ${applyTheme("color_coolGrey500")};
+`;
+
+const Discount = styled.span`
+  color: ${applyTheme("color_forestGreen300")};
+  font-weight: ${applyTheme("font_weight_bold")};
+`;
+
+const Total = styled.span`
+  font-weight: ${applyTheme("font_weight_bold")};
+  color: ${applyTheme("color_coolGrey500")};
+`;
+
+class CartSummary extends Component {
+  static propTypes = {
+    /**
+     * Discount amount associated with promo code
+     */
+    displayDiscount: PropTypes.string,
+    /**
+     * Shipping cost
+     */
+    displayShipping: PropTypes.string,
+    /**
+     * Subtotal amount
+     */
+    displaySubtotal: PropTypes.string.isRequired,
+    /**
+     * Calculated tax amount
+     */
+    displayTax: PropTypes.string,
+    /**
+     * Total amount
+     */
+    displayTotal: PropTypes.string.isRequired,
+    /**
+     * If a product qualifies for free shipping, display "FREE" for shipping method
+     */
+    isFreeShipping: PropTypes.bool,
+    /**
+     * Quantity of products in shopping cart
+     */
+    itemsQuantity: PropTypes.number.isRequired
+  }
+
+  renderDiscount = () => {
+    const { displayDiscount } = this.props;
+
+    return (
+      <tr>
+        <Td>Promo code applied</Td>
+        <TdValue><Discount>{displayDiscount}</Discount></TdValue>
+      </tr>
+    );
+  }
+
+  render() {
+    const {
+      displayDiscount,
+      displayShipping,
+      displaySubtotal,
+      displayTax,
+      displayTotal,
+      isFreeShipping,
+      itemsQuantity
+    } = this.props;
+
+    const shipping = (isFreeShipping) ? "FREE" : displayShipping;
+    const tax = displayTax || "-";
+    const discount = displayDiscount && this.renderDiscount();
+
+    return (
+      <Table>
+        <thead>
+          <tr>
+            <Th>
+              <Title>
+                  Cart Summary
+              </Title>
+            </Th>
+            <Thr>
+              {itemsQuantity} items
+            </Thr>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <Td>Subtotal</Td>
+            <TdValue>{displaySubtotal}</TdValue>
+          </tr>
+          <tr>
+            <Td>Shipping</Td>
+            <TdValue>{shipping}</TdValue>
+          </tr>
+          {discount}
+          <tr>
+            <Td>Tax</Td>
+            <TdValue>{tax}</TdValue>
+          </tr>
+          <tr>
+            <Td>Total</Td>
+            <TdValue><Total>{displayTotal}</Total></TdValue>
+          </tr>
+        </tbody>
+      </Table>
+    );
+  }
+}
+
+export default CartSummary;
diff --git a/package/src/components/CartSummary/v1/CartSummary.md b/package/src/components/CartSummary/v1/CartSummary.md
new file mode 100644
index 00000000..2947adef
--- /dev/null
+++ b/package/src/components/CartSummary/v1/CartSummary.md
@@ -0,0 +1,47 @@
+### CartSummary
+
+### Overview
+Displays a summary of the current current items in the cart.
+
+#### Basic usage
+```jsx
+<CartSummary
+  displayShipping="$10.99"
+  displaySubtotal="$275.77"
+  displayTotal="$286.10"
+  itemsQuantity={3}
+/>
+```
+
+#### Renders summary with FREE shipping
+```jsx
+<CartSummary
+  displaySubtotal="$275.77"
+  displayTotal="$275.77"
+  itemsQuantity={3}
+  isFreeShipping
+/>
+```
+
+#### Renders summary with calculated taxes
+```jsx
+<CartSummary
+  displayShipping="$5.25"
+  displaySubtotal="$275.77"
+  displayTotal="$288.64"
+  itemsQuantity={3}
+  displayTax="$7.62"
+/>
+```
+
+#### Renders summary with applied discount
+```jsx
+<CartSummary
+  displayDiscount="-$83.42"
+  displayShipping="$5.25"
+  displaySubtotal="$275.77"
+  displayTotal="$288.64"
+  itemsQuantity={3}
+  displayTax="$7.62"
+/>
+```
diff --git a/package/src/components/CartSummary/v1/index.js b/package/src/components/CartSummary/v1/index.js
new file mode 100644
index 00000000..fd4ac7aa
--- /dev/null
+++ b/package/src/components/CartSummary/v1/index.js
@@ -0,0 +1,1 @@
+export { default } from "./CartSummary";
diff --git a/package/src/components/StockWarning/v1/StockWarning.js b/package/src/components/StockWarning/v1/StockWarning.js
new file mode 100644
index 00000000..3aeedb9e
--- /dev/null
+++ b/package/src/components/StockWarning/v1/StockWarning.js
@@ -0,0 +1,34 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import styled from "styled-components";
+import { applyTheme } from "../../../utils";
+
+const Span = styled.div`
+  color: ${applyTheme("color_stockWarning")};
+`;
+
+class StockWarning extends Component {
+  static propTypes = {
+    /**
+     * The product's current stock level
+     */
+    inventoryQuantity: PropTypes.number.isRequired,
+    /**
+     * When true, indicates that a product's inventory level has reached
+     * the low level threshold.
+     */
+    isLowInventoryQuantity: PropTypes.bool.isRequired
+  };
+
+  render() {
+    const { inventoryQuantity, isLowInventoryQuantity } = this.props;
+
+    if (!isLowInventoryQuantity) return null;
+
+    return (
+      <Span>Only { inventoryQuantity } in stock</Span>
+    );
+  }
+}
+
+export default StockWarning;
diff --git a/package/src/components/StockWarning/v1/StockWarning.md b/package/src/components/StockWarning/v1/StockWarning.md
new file mode 100644
index 00000000..a035c3d5
--- /dev/null
+++ b/package/src/components/StockWarning/v1/StockWarning.md
@@ -0,0 +1,14 @@
+### StockWarning
+
+#### Overview
+The `StockWarning` displays a low inventory warning when the `isLowInventoryQuantity` prop is true.
+
+#### An inventory warning will be rendered when the `isLowInventoryQuantity` prop is `true`
+```jsx
+  <StockWarning inventoryQuantity={10} isLowInventoryQuantity={true} />
+```
+
+#### It's not rendered when a product has a normal inventory level
+```jsx
+  <StockWarning inventoryQuantity={10} isLowInventoryQuantity={false} />
+```
diff --git a/package/src/components/StockWarning/v1/index.js b/package/src/components/StockWarning/v1/index.js
new file mode 100644
index 00000000..0b886b6f
--- /dev/null
+++ b/package/src/components/StockWarning/v1/index.js
@@ -0,0 +1,1 @@
+export { default } from "./StockWarning";
diff --git a/package/src/defaultComponentTheme.js b/package/src/defaultComponentTheme.js
index bdf5ef85..d3623892 100644
--- a/package/src/defaultComponentTheme.js
+++ b/package/src/defaultComponentTheme.js
@@ -153,10 +153,23 @@ const borderRadius = baseUnit(0.2);
 const defaultStyles = {
   rui_color_default: black55,
   rui_color_disabled: black25,
+  rui_color_black02: black02,
+  rui_color_black30: black30,
   rui_color_error: red,
+  rui_color_stockWarning: red,
   rui_color_success: teal,
   rui_color_coolGrey: coolGrey,
+  rui_color_coolGrey100: coolGrey100,
+  rui_color_coolGrey200: coolGrey200,
+  rui_color_coolGrey300: coolGrey300,
+  rui_color_coolGrey400: coolGrey400,
+  rui_color_coolGrey500: coolGrey500,
+  rui_color_coolGrey600: coolGrey600,
+  rui_color_forestGreen300: forestGreen300,
   rui_color_white: white,
+  rui_font_weight_light: fontWeightLight,
+  rui_font_weight_normal: fontWeightRegular,
+  rui_font_weight_bold: fontWeightBold,
   rui_leading_body: bodyLeading,
   rui_leading_header: headerLeading,
   rui_leading_flat: flatLeading
diff --git a/styleguide.config.js b/styleguide.config.js
index 2e48f39a..6bb1809d 100644
--- a/styleguide.config.js
+++ b/styleguide.config.js
@@ -93,10 +93,15 @@ module.exports = {
           name: "Actions"
         }),
         generateSection({
-          componentNames: ["Price"],
+          componentNames: ["Price", "StockWarning"],
           content: "styleguide/src/sections/Product.md",
           name: "Product"
         }),
+        generateSection({
+          componentNames: ["CartSummary"],
+          content: "styleguide/src/sections/Cart.md",
+          name: "Cart"
+        }),
         generateSection({
           componentNames: ["ErrorsBlock", "Field", "Select", "TextInput"],
           content: "styleguide/src/sections/Forms.md",
diff --git a/styleguide/src/sections/Cart.md b/styleguide/src/sections/Cart.md
new file mode 100644
index 00000000..da2c2f8a
--- /dev/null
+++ b/styleguide/src/sections/Cart.md
@@ -0,0 +1,1 @@
+TODO Write introduction to Cart


Test patch:
diff --git a/package/src/components/CartSummary/v1/CartSummary.test.js b/package/src/components/CartSummary/v1/CartSummary.test.js
new file mode 100644
index 00000000..0aae7507
--- /dev/null
+++ b/package/src/components/CartSummary/v1/CartSummary.test.js
@@ -0,0 +1,62 @@
+import React from "react";
+import renderer from "react-test-renderer";
+import CartSummary from "./CartSummary";
+
+test("Displays a summary of the current items in the cart", () => {
+  const component = renderer.create((
+    <CartSummary
+      displayShipping="3-5 Days"
+      displaySubtotal="$246.22"
+      displayTotal="$300.424"
+      itemsQuantity={3}
+    />
+  ));
+
+  const tree = component.toJSON();
+  expect(tree).toMatchSnapshot();
+});
+
+test("Displays a summary with FREE shipping", () => {
+  const component = renderer.create((
+    <CartSummary
+      displayShipping="3-5 Days"
+      displaySubtotal="$246.22"
+      displayTotal="$300.424"
+      isFreeShipping
+      itemsQuantity={3}
+    />
+  ));
+
+  const tree = component.toJSON();
+  expect(tree).toMatchSnapshot();
+});
+
+test("Displays a summary with calculated taxes", () => {
+  const component = renderer.create((
+    <CartSummary
+      displayShipping="3-5 Days"
+      displaySubtotal="$246.22"
+      displayTotal="$300.424"
+      displayTax="$24.88"
+      itemsQuantity={3}
+    />
+  ));
+
+  const tree = component.toJSON();
+  expect(tree).toMatchSnapshot();
+});
+
+test("Displays a summary with applied discount", () => {
+  const component = renderer.create((
+    <CartSummary
+      displayDiscount="-$80.42"
+      displayShipping="3-5 Days"
+      displaySubtotal="$246.22"
+      displayTotal="$300.424"
+      itemsQuantity={3}
+    />
+  ));
+
+  const tree = component.toJSON();
+  expect(tree).toMatchSnapshot();
+});
diff --git a/package/src/components/CartSummary/v1/__snapshots__/CartSummary.test.js.snap b/package/src/components/CartSummary/v1/__snapshots__/CartSummary.test.js.snap
new file mode 100644
index 00000000..74d88748
--- /dev/null
+++ b/package/src/components/CartSummary/v1/__snapshots__/CartSummary.test.js.snap
@@ -0,0 +1,490 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Displays a summary of the current items in the cart 1`] = `
+.c0 {
+  width: 100%;
+  background-color: #fafafa;
+  padding: 1rem;
+}
+
+.c1 {
+  text-align: left;
+}
+
+.c3 {
+  text-align: right;
+  color: #b3b3b3;
+  font-weight: normal;
+}
+
+.c5 {
+  text-align: right;
+  padding: 1rem 0;
+  color: #3c3c3c;
+}
+
+.c4 {
+  padding: 1rem 0;
+  color: #3c5d6f;
+}
+
+.c2 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+.c6 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+<table
+  className="c0"
+>
+  <thead>
+    <tr>
+      <th
+        className="c1"
+      >
+        <span
+          className="c2"
+        >
+          Cart Summary
+        </span>
+      </th>
+      <th
+        className="c3"
+      >
+        3
+         items
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td
+        className="c4"
+      >
+        Subtotal
+      </td>
+      <td
+        className="c5"
+      >
+        $246.22
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Shipping
+      </td>
+      <td
+        className="c5"
+      >
+        3-5 Days
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Tax
+      </td>
+      <td
+        className="c5"
+      >
+        -
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Total
+      </td>
+      <td
+        className="c5"
+      >
+        <span
+          className="c6"
+        >
+          $300.424
+        </span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+`;
+
+exports[`Displays a summary with FREE shipping 1`] = `
+.c0 {
+  width: 100%;
+  background-color: #fafafa;
+  padding: 1rem;
+}
+
+.c1 {
+  text-align: left;
+}
+
+.c3 {
+  text-align: right;
+  color: #b3b3b3;
+  font-weight: normal;
+}
+
+.c5 {
+  text-align: right;
+  padding: 1rem 0;
+  color: #3c3c3c;
+}
+
+.c4 {
+  padding: 1rem 0;
+  color: #3c5d6f;
+}
+
+.c2 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+.c6 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+<table
+  className="c0"
+>
+  <thead>
+    <tr>
+      <th
+        className="c1"
+      >
+        <span
+          className="c2"
+        >
+          Cart Summary
+        </span>
+      </th>
+      <th
+        className="c3"
+      >
+        3
+         items
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td
+        className="c4"
+      >
+        Subtotal
+      </td>
+      <td
+        className="c5"
+      >
+        $246.22
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Shipping
+      </td>
+      <td
+        className="c5"
+      >
+        FREE
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Tax
+      </td>
+      <td
+        className="c5"
+      >
+        -
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Total
+      </td>
+      <td
+        className="c5"
+      >
+        <span
+          className="c6"
+        >
+          $300.424
+        </span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+`;
+
+exports[`Displays a summary with applied discount 1`] = `
+.c0 {
+  width: 100%;
+  background-color: #fafafa;
+  padding: 1rem;
+}
+
+.c1 {
+  text-align: left;
+}
+
+.c3 {
+  text-align: right;
+  color: #b3b3b3;
+  font-weight: normal;
+}
+
+.c5 {
+  text-align: right;
+  padding: 1rem 0;
+  color: #3c3c3c;
+}
+
+.c4 {
+  padding: 1rem 0;
+  color: #3c5d6f;
+}
+
+.c2 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+.c6 {
+  color: #0db781;
+  font-weight: 700;
+}
+
+.c7 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+<table
+  className="c0"
+>
+  <thead>
+    <tr>
+      <th
+        className="c1"
+      >
+        <span
+          className="c2"
+        >
+          Cart Summary
+        </span>
+      </th>
+      <th
+        className="c3"
+      >
+        3
+         items
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td
+        className="c4"
+      >
+        Subtotal
+      </td>
+      <td
+        className="c5"
+      >
+        $246.22
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Shipping
+      </td>
+      <td
+        className="c5"
+      >
+        3-5 Days
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Promo code applied
+      </td>
+      <td
+        className="c5"
+      >
+        <span
+          className="c6"
+        >
+          -$80.42
+        </span>
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Tax
+      </td>
+      <td
+        className="c5"
+      >
+        -
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Total
+      </td>
+      <td
+        className="c5"
+      >
+        <span
+          className="c7"
+        >
+          $300.424
+        </span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+`;
+
+exports[`Displays a summary with calculated taxes 1`] = `
+.c0 {
+  width: 100%;
+  background-color: #fafafa;
+  padding: 1rem;
+}
+
+.c1 {
+  text-align: left;
+}
+
+.c3 {
+  text-align: right;
+  color: #b3b3b3;
+  font-weight: normal;
+}
+
+.c5 {
+  text-align: right;
+  padding: 1rem 0;
+  color: #3c3c3c;
+}
+
+.c4 {
+  padding: 1rem 0;
+  color: #3c5d6f;
+}
+
+.c2 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+.c6 {
+  font-weight: 700;
+  color: #3c3c3c;
+}
+
+<table
+  className="c0"
+>
+  <thead>
+    <tr>
+      <th
+        className="c1"
+      >
+        <span
+          className="c2"
+        >
+          Cart Summary
+        </span>
+      </th>
+      <th
+        className="c3"
+      >
+        3
+         items
+      </th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr>
+      <td
+        className="c4"
+      >
+        Subtotal
+      </td>
+      <td
+        className="c5"
+      >
+        $246.22
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Shipping
+      </td>
+      <td
+        className="c5"
+      >
+        3-5 Days
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Tax
+      </td>
+      <td
+        className="c5"
+      >
+        $24.88
+      </td>
+    </tr>
+    <tr>
+      <td
+        className="c4"
+      >
+        Total
+      </td>
+      <td
+        className="c5"
+      >
+        <span
+          className="c6"
+        >
+          $300.424
+        </span>
+      </td>
+    </tr>
+  </tbody>
+</table>
+`;
diff --git a/package/src/components/StockWarning/v1/StockWarning.test.js b/package/src/components/StockWarning/v1/StockWarning.test.js
new file mode 100644
index 00000000..f3abb599
--- /dev/null
+++ b/package/src/components/StockWarning/v1/StockWarning.test.js
@@ -0,0 +1,27 @@
+import React from "react";
+import renderer from "react-test-renderer";
+import checkPropTypes from "check-prop-types";
+import StockWarning from "./StockWarning";
+
+test("Displays error warning about required props", () => {
+  const errorMessage = checkPropTypes(StockWarning.propTypes, {});
+  expect(errorMessage).toMatchSnapshot();
+});
+
+test("Renders stock warning when inventory is low", () => {
+  const component = renderer.create((
+    <StockWarning inventoryQuantity={10} isLowInventoryQuantity={true} />
+  ));
+
+  const tree = component.toJSON();
+  expect(tree).toMatchSnapshot();
+});
+
+test("Renders nothing when stock level is normal", () => {
+  const component = renderer.create((
+    <StockWarning inventoryQuantity={10} isLowInventoryQuantity={false} />
+  ));
+
+  const tree = component.toJSON();
+  expect(tree).toMatchSnapshot();
+});
diff --git a/package/src/components/StockWarning/v1/__snapshots__/StockWarning.test.js.snap b/package/src/components/StockWarning/v1/__snapshots__/StockWarning.test.js.snap
new file mode 100644
index 00000000..4956d0d5
--- /dev/null
+++ b/package/src/components/StockWarning/v1/__snapshots__/StockWarning.test.js.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Displays error warning about required props 1`] = `"Failed undefined type: The undefined \`inventoryQuantity\` is marked as required in \`<<anonymous>>\`, but its value is \`undefined\`."`;
+
+exports[`Renders nothing when stock level is normal 1`] = `null`;
+
+exports[`Renders stock warning when inventory is low 1`] = `
+.c0 {
+  color: #cd3f4c;
+}
+
+<div
+  className="c0"
+>
+  Only 
+  10
+   in stock
+</div>
+`;