From c7eb99dc765d0cb51e0157c19875c1263dc6b86a Mon Sep 17 00:00:00 2001
From: Praveen Reghunathan Nair <pnair@nisum.com>
Date: Thu, 6 Feb 2020 15:26:42 -0800
Subject: [PATCH] Implementing PDP page

---
 src/components/Footer/Footer.module.scss      |  2 +-
 src/components/Header/Header.module.scss      |  1 +
 .../LoadingAnimation.module.scss              |  2 +-
 src/index.js                                  |  3 ++
 src/models/image.js                           | 29 +++++++++++++++
 src/models/listingProduct.js                  | 28 ++++++++++++++
 src/models/product.js                         | 26 +++++++++++++
 src/models/productDetail.js                   | 16 ++++++++
 src/pages/PDP/PDP.jsx                         | 29 +++++++++++++--
 src/pages/PDP/PDP.module.scss                 | 20 ++++++++++
 src/pages/PDP/index.js                        | 18 ++++++++-
 src/pages/PLP/PLP.jsx                         | 21 +++++++++--
 src/pages/PLP/PLP.module.scss                 | 15 ++++++++
 src/pages/PLP/ProductItem/ProductItem.jsx     | 33 +++++++++++++++++
 .../PLP/ProductItem/ProductItem.module.scss   | 23 ++++++++++++
 src/pages/PLP/ProductItem/ProductItem.spec.js |  0
 src/pages/PLP/ProductItem/index.js            |  3 ++
 src/pages/PLP/index.js                        | 12 +++++-
 src/redux/actions/product.js                  | 23 ++++++++++++
 src/redux/constants/actionTypes.js            | 10 +++++
 src/redux/reducers/category.js                |  3 +-
 src/redux/reducers/index.js                   |  2 +
 src/redux/reducers/product.js                 | 37 +++++++++++++++++++
 src/services/api.js                           | 10 ++++-
 src/styles/_variables.scss                    |  4 +-
 25 files changed, 357 insertions(+), 13 deletions(-)
 create mode 100644 src/models/image.js
 create mode 100644 src/models/listingProduct.js
 create mode 100644 src/models/product.js
 create mode 100644 src/models/productDetail.js
 create mode 100644 src/pages/PLP/ProductItem/ProductItem.jsx
 create mode 100644 src/pages/PLP/ProductItem/ProductItem.module.scss
 create mode 100644 src/pages/PLP/ProductItem/ProductItem.spec.js
 create mode 100644 src/pages/PLP/ProductItem/index.js
 create mode 100644 src/redux/actions/product.js
 create mode 100644 src/redux/reducers/product.js

diff --git a/src/components/Footer/Footer.module.scss b/src/components/Footer/Footer.module.scss
index f676223..277321b 100644
--- a/src/components/Footer/Footer.module.scss
+++ b/src/components/Footer/Footer.module.scss
@@ -1,5 +1,5 @@
 .appFooter {
-    position: fixed;
+    // position: fixed;
     bottom: 0;
     left: 0;
 }
\ No newline at end of file
diff --git a/src/components/Header/Header.module.scss b/src/components/Header/Header.module.scss
index 9570d40..d837132 100644
--- a/src/components/Header/Header.module.scss
+++ b/src/components/Header/Header.module.scss
@@ -8,6 +8,7 @@
     left: 0;
     width: 100%;
     box-shadow: 0 0 2px 0 rgba-background($primary-black, .24), 0 2px 2px 0 rgba-background($primary-black, .12);
+    background: $primary-white;
     @include flex-box();
 
     h1 {
diff --git a/src/components/LoadingAnimation/LoadingAnimation.module.scss b/src/components/LoadingAnimation/LoadingAnimation.module.scss
index 9d9f6b5..4dbcd25 100644
--- a/src/components/LoadingAnimation/LoadingAnimation.module.scss
+++ b/src/components/LoadingAnimation/LoadingAnimation.module.scss
@@ -6,7 +6,7 @@
     height: 100%;
 
     .loader {
-        border: 16px solid $gray;
+        border: 16px solid $gray1;
         border-radius: 50%;
         border-top: 16px solid $primary;
         width: 120px;
diff --git a/src/index.js b/src/index.js
index f5b16ff..b89811c 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,6 +1,7 @@
 import React from 'react';
 import ReactDOM from 'react-dom';
 import { Provider } from 'react-redux';
+import api from 'services/api';
 
 import App from 'App';
 import configureStore from 'redux/configureStore';
@@ -19,6 +20,8 @@ export function main(args = {}) {
 
     if (renderNow) {
 
+        api.init(store);
+
         reactDOM.render(
             <Provider store={store}>
                 <App />
diff --git a/src/models/image.js b/src/models/image.js
new file mode 100644
index 0000000..f124ebf
--- /dev/null
+++ b/src/models/image.js
@@ -0,0 +1,29 @@
+
+class Image {
+    constructor(data) {
+        this.data = data || {};
+    }
+
+    get width() {
+        const { width } = this.data;
+        return width;
+    }
+
+    get height() {
+        const { height } = this.data;
+        return height;
+    }
+
+    get alt() {
+        const { alt } = this.data;
+        return alt || 'product image';
+    }
+
+    get url() {
+        const { href: url } = this.data;
+        return url;
+    }
+
+}
+
+export default Image;
\ No newline at end of file
diff --git a/src/models/listingProduct.js b/src/models/listingProduct.js
new file mode 100644
index 0000000..5c97a2b
--- /dev/null
+++ b/src/models/listingProduct.js
@@ -0,0 +1,28 @@
+import Product from 'models/product';
+import Image from 'models/image';
+
+import ROUTES from 'constants/routes';
+
+class ListingProduct extends Product {
+    constructor(data) {
+        super(data);
+        this._image = new Image(this.data.thumbnail);
+    }
+
+    get image() {
+        return this._image;
+    }
+
+    get sellingPrice() {
+        const { high, low } = this.data.priceRange.selling;
+        return `$${low} - $${high}`;
+    }
+
+    get detailsPageUrl() {
+        const { id } = this.data;
+        return ROUTES.details.getUrl(id);
+    }
+
+}
+
+export default ListingProduct;
\ No newline at end of file
diff --git a/src/models/product.js b/src/models/product.js
new file mode 100644
index 0000000..60d9374
--- /dev/null
+++ b/src/models/product.js
@@ -0,0 +1,26 @@
+
+import Image from 'models/image';
+
+class Product {
+    constructor(data) {
+        this.data = data || {};
+        this._image = new Image(this.data.hero);
+    }
+
+    get name() {
+        const { name = '' } = this.data;
+        return name.replace(/&amp;/, '&');
+    }
+
+    get image() {
+        return this._image;
+    }
+
+    get sellingPrice() {
+        const { priceRange: { selling: { high, low } = {} } = {} } = this.data;
+        return `$${low} - $${high}`;
+    }
+
+}
+
+export default Product;
\ No newline at end of file
diff --git a/src/models/productDetail.js b/src/models/productDetail.js
new file mode 100644
index 0000000..4ea93b1
--- /dev/null
+++ b/src/models/productDetail.js
@@ -0,0 +1,16 @@
+import Product from 'models/product';
+import Image from 'models/image';
+
+class ProductDetails extends Product {
+    constructor(data) {
+        super(data);
+        const { images = [] } = this.data;
+        this._images = images.map(item => new Image(item));
+    }
+
+    get images() {
+        return this._images;
+    }
+}
+
+export default ProductDetails;
\ No newline at end of file
diff --git a/src/pages/PDP/PDP.jsx b/src/pages/PDP/PDP.jsx
index d160157..3b5db87 100644
--- a/src/pages/PDP/PDP.jsx
+++ b/src/pages/PDP/PDP.jsx
@@ -1,14 +1,37 @@
 import React, { Component } from 'react';
-import styles from './PDP.module.scss';
+import style from './PDP.module.scss';
+import Product from 'models/product';
 
 class PDPPage extends Component {
+
+    componentDidMount() {
+        const { match: { params: { productId } } } = this.props;
+        this.props.getProductDetails(productId);
+    }
+
     render() {
+        const image = { url: 'https://www.westelm.com/weimgs/rk/images/wcm/products/202003/0005/belgian-linen-ladder-stripe-embroidery-duvet-cover-shams-s-m.jpg' };
+        const { product } = this.props;
+        const { name, } = new Product(product);
         return (
-            <section className={styles.PDPPage}>
-                PDP Page!
+            <section className={style.PDPPage}>
+                <div className={style.content}>
+                    <div className={style.images}>
+                        <img
+                            src={image.url}
+                            alt={image.alt}
+                            className={style.productImage}
+                        />
+                    </div>
+                    <div className={style.details}>
+                        <h1>{name}</h1>
+                        <div>Price: 44 - 329</div>
+                    </div>
+                </div>
             </section>
         );
     }
+
 }
 
 export default PDPPage;
\ No newline at end of file
diff --git a/src/pages/PDP/PDP.module.scss b/src/pages/PDP/PDP.module.scss
index 3e17385..38f21c3 100644
--- a/src/pages/PDP/PDP.module.scss
+++ b/src/pages/PDP/PDP.module.scss
@@ -2,4 +2,24 @@
 
 .PDPPage {
     margin-top: $large-footer-height;
+
+    h1 {
+        color: $gray2;
+    }
+
+    .content {
+        display: flex;
+        align-items: flex-start;
+        justify-content: space-evenly;
+        box-sizing: border-box;
+        padding: 15px;
+        flex-wrap: wrap;
+
+        .images {}
+
+        .details {
+            // width: 50%;
+        }
+
+    }
 }
\ No newline at end of file
diff --git a/src/pages/PDP/index.js b/src/pages/PDP/index.js
index 5d6a252..a4a4376 100644
--- a/src/pages/PDP/index.js
+++ b/src/pages/PDP/index.js
@@ -1,3 +1,19 @@
+import { connect } from 'react-redux';
+import { getProductDetails } from 'redux/actions/product';
+
 import PDPPage from './PDP';
 
-export default PDPPage;
\ No newline at end of file
+const mapStateToProps = (state) => {
+    const { product, loading, error } = state.product;
+    return {
+        product,
+        loading,
+        error
+    }
+};
+
+const mapDispatchToProps = {
+    getProductDetails
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(PDPPage);
\ No newline at end of file
diff --git a/src/pages/PLP/PLP.jsx b/src/pages/PLP/PLP.jsx
index e16353e..3d7e2b8 100644
--- a/src/pages/PLP/PLP.jsx
+++ b/src/pages/PLP/PLP.jsx
@@ -1,11 +1,26 @@
 import React, { Component } from 'react';
-import styles from './PLP.module.scss';
+import style from './PLP.module.scss';
+import ProductItem from './ProductItem';
+import ListingProduct from 'models/listingProduct';
 
 class PLPPage extends Component {
     render() {
+        const { name, items } = this.props;
         return (
-            <section className={styles.PLPPage}>
-                PLP Page!
+            <section className={style.PLPPage}>
+                <h1>{name}</h1>
+                <ul className={style.products}>
+                    {
+                        items.map(item => {
+                            return (
+                                <ProductItem
+                                    key={item.id}
+                                    data={new ListingProduct(item)}
+                                />
+                            )
+                        })
+                    }
+                </ul>
             </section>
         );
     }
diff --git a/src/pages/PLP/PLP.module.scss b/src/pages/PLP/PLP.module.scss
index b136f00..6f147d2 100644
--- a/src/pages/PLP/PLP.module.scss
+++ b/src/pages/PLP/PLP.module.scss
@@ -1,5 +1,20 @@
 @import '~styles/variables';
+@import '~styles/mixins';
 
 .PLPPage {
     margin-top: $large-footer-height;
+    padding: 10px;
+
+    h1 {
+        margin: 0;
+        margin-left: 10px;
+        color: $gray2;
+    }
+
+    .products {
+        margin: 0;
+        padding: 0;
+        display: flex;
+        flex-wrap: wrap;
+    }
 }
\ No newline at end of file
diff --git a/src/pages/PLP/ProductItem/ProductItem.jsx b/src/pages/PLP/ProductItem/ProductItem.jsx
new file mode 100644
index 0000000..0719975
--- /dev/null
+++ b/src/pages/PLP/ProductItem/ProductItem.jsx
@@ -0,0 +1,33 @@
+import React, { Component } from 'react';
+import style from './ProductItem.module.scss';
+
+class ProductItem extends Component {
+
+    render() {
+
+        const { data = {} } = this.props;
+        const { name, image, sellingPrice, detailsPageUrl } = data;
+
+        return (
+            <li className={style.productItem}>
+                <a href={detailsPageUrl}>
+                    <img
+                        src={image.url}
+                        alt={image.alt}
+                        className={style.productImage}
+                    />
+                </a>
+                <div className={style.details}>
+                    <a className={style.title} href={detailsPageUrl}>{name}</a>
+                    <div className={style.sellingPrice}>
+                        {sellingPrice}
+                    </div>
+                </div>
+            </li>
+        );
+    }
+
+}
+
+export default ProductItem;
+
diff --git a/src/pages/PLP/ProductItem/ProductItem.module.scss b/src/pages/PLP/ProductItem/ProductItem.module.scss
new file mode 100644
index 0000000..bdbc014
--- /dev/null
+++ b/src/pages/PLP/ProductItem/ProductItem.module.scss
@@ -0,0 +1,23 @@
+@import '~styles/variables';
+
+.productItem {
+    list-style: none;
+    max-height: 420px;
+    max-width: 350px;
+    box-sizing: border-box;
+    padding: 10px;
+
+    .productImage {
+        width: 100%;
+    }
+
+    .details {
+        padding: 5px;
+    }
+
+    .sellingPrice {
+        color: $primary;
+        font-weight: bold;
+        margin-top: 5px;
+    }
+}
\ No newline at end of file
diff --git a/src/pages/PLP/ProductItem/ProductItem.spec.js b/src/pages/PLP/ProductItem/ProductItem.spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/pages/PLP/ProductItem/index.js b/src/pages/PLP/ProductItem/index.js
new file mode 100644
index 0000000..eb687b4
--- /dev/null
+++ b/src/pages/PLP/ProductItem/index.js
@@ -0,0 +1,3 @@
+import ProductItem from './ProductItem';
+
+export default ProductItem;
\ No newline at end of file
diff --git a/src/pages/PLP/index.js b/src/pages/PLP/index.js
index b97aa58..4cdf005 100644
--- a/src/pages/PLP/index.js
+++ b/src/pages/PLP/index.js
@@ -1,3 +1,13 @@
+import { connect } from 'react-redux';
+
 import PLPPage from './PLP';
 
-export default PLPPage;
\ No newline at end of file
+const mapStateToProps = ({ category }) => {
+    const { items, name } = category;
+    return {
+        items,
+        name
+    }
+};
+
+export default connect(mapStateToProps, null)(PLPPage);
diff --git a/src/redux/actions/product.js b/src/redux/actions/product.js
new file mode 100644
index 0000000..844e15c
--- /dev/null
+++ b/src/redux/actions/product.js
@@ -0,0 +1,23 @@
+
+import API from 'services/api';
+import { PRODUCT_ACTION_TYPE } from 'redux/constants/actionTypes';
+
+export function getProductDetails(id) {
+
+    const { PRODUCT_INIT_BEGIN, PRODUCT_INIT_SUCCESS, PRODUCT_INIT_FAILED } = PRODUCT_ACTION_TYPE;
+
+    return async dispatch => {
+
+        dispatch({ type: PRODUCT_INIT_BEGIN });
+
+        try {
+            const product = await API.getProductDetails(id);
+            dispatch({ type: PRODUCT_INIT_SUCCESS, payload: product });
+        }
+        catch (err) {
+            console.log(err);
+            dispatch({ type: PRODUCT_INIT_FAILED, payload: err });
+        }
+
+    };
+}
\ No newline at end of file
diff --git a/src/redux/constants/actionTypes.js b/src/redux/constants/actionTypes.js
index 000855d..17b7ac3 100644
--- a/src/redux/constants/actionTypes.js
+++ b/src/redux/constants/actionTypes.js
@@ -7,4 +7,14 @@ export const CATEGORY_ACTION_TYPE = Object.freeze({
     CATEGORY_INIT_BEGIN,
     CATEGORY_INIT_SUCCESS,
     CATEGORY_INIT_FAILED,
+});
+
+const PRODUCT_INIT_BEGIN = 'PRODUCT_INIT_BEGIN';
+const PRODUCT_INIT_SUCCESS = 'PRODUCT_INIT_SUCCESS';
+const PRODUCT_INIT_FAILED = 'PRODUCT_INIT_FAILED';
+
+export const PRODUCT_ACTION_TYPE = Object.freeze({
+    PRODUCT_INIT_BEGIN,
+    PRODUCT_INIT_SUCCESS,
+    PRODUCT_INIT_FAILED,
 });
\ No newline at end of file
diff --git a/src/redux/reducers/category.js b/src/redux/reducers/category.js
index ab17b20..1b27693 100644
--- a/src/redux/reducers/category.js
+++ b/src/redux/reducers/category.js
@@ -21,7 +21,8 @@ export default function categoryReducer(state = initialState, action) {
         case CATEGORY_INIT_SUCCESS: {
             return {
                 ...state,
-                loading: false
+                loading: false,
+                ...action.payload
             }
         }
         case CATEGORY_INIT_FAILURE: {
diff --git a/src/redux/reducers/index.js b/src/redux/reducers/index.js
index 0184284..c5246f0 100644
--- a/src/redux/reducers/index.js
+++ b/src/redux/reducers/index.js
@@ -1,9 +1,11 @@
 import { combineReducers } from 'redux';
 
 import categoryReducer from './category';
+import productReducer from './product';
 
 const rootReducer = combineReducers({
     category: categoryReducer,
+    product: productReducer
 });
 
 export default rootReducer;
diff --git a/src/redux/reducers/product.js b/src/redux/reducers/product.js
new file mode 100644
index 0000000..fe345ee
--- /dev/null
+++ b/src/redux/reducers/product.js
@@ -0,0 +1,37 @@
+
+import { PRODUCT_ACTION_TYPE } from 'redux/constants/actionTypes';
+
+const { PRODUCT_INIT_BEGIN, PRODUCT_INIT_SUCCESS, PRODUCT_INIT_FAILURE } = PRODUCT_ACTION_TYPE;
+
+const initialState = {
+    loading: true,
+    error: null,
+    product: null
+};
+
+export default function productReducer(state = initialState, action) {
+    switch (action.type) {
+        case PRODUCT_INIT_BEGIN: {
+            return {
+                ...state,
+                loading: true
+            }
+        }
+        case PRODUCT_INIT_SUCCESS: {
+            return {
+                ...state,
+                loading: false,
+                product: action.payload
+            }
+        }
+        case PRODUCT_INIT_FAILURE: {
+            return {
+                ...state,
+                loading: false,
+                error: action.payload,
+            }
+        }
+        default:
+            return state;
+    }
+}
\ No newline at end of file
diff --git a/src/services/api.js b/src/services/api.js
index 74d2c2e..a9ed4cc 100644
--- a/src/services/api.js
+++ b/src/services/api.js
@@ -16,15 +16,23 @@ async function get(url) {
                 StatusText: ${response.statusText}`;
         throw Error(message);
     }
-
 }
 
 class ApiClient {
 
+    init(store) {
+        this.store = store;
+    }
+
     async getCategoryData() {
         return get(URL.CATEGORY);
     }
 
+    async getProductDetails(id) {
+        const { category: { items = [] } = {} } = this.store.getState();
+        const product = items.find(item => item.id === id);
+        return Promise.resolve(product)
+    }
 }
 
 export default new ApiClient();
\ No newline at end of file
diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss
index b0720f7..3b4536e 100644
--- a/src/styles/_variables.scss
+++ b/src/styles/_variables.scss
@@ -1,6 +1,8 @@
 // Primary
 $primary : #866347;
 $primary-black : #333333;
-$gray : #F5F6F8;
+$primary-white : white;
+$gray1 : #F5F6F8;
+$gray2 : gray;
 
 $large-footer-height: 60px;
\ No newline at end of file
-- 
2.18.1