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(/&/, '&'); + } + + 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