├── .npmrc ├── _config.yml ├── config ├── jest │ ├── CSSStub.js │ └── FileStub.js ├── polyfills.js ├── env.js └── paths.js ├── static.json ├── public ├── favicon.ico └── index.html ├── src ├── images │ ├── b1.jpg │ ├── b2.jpg │ ├── bodybg.png │ ├── logo.png │ ├── logo2.png │ └── loader.svg ├── constants │ ├── app-defaults.js │ ├── supported-locales.js │ ├── app-routes.js │ └── app-actions.js ├── components │ ├── styles │ │ ├── core │ │ │ ├── fonts.scss │ │ │ ├── variables.scss │ │ │ └── modal.scss │ │ ├── components │ │ │ ├── mobile-nav.scss │ │ │ ├── footer.scss │ │ │ ├── home-slider.scss │ │ │ └── product-tile.scss │ │ ├── pages │ │ │ ├── orders.scss │ │ │ ├── cart.scss │ │ │ ├── checkout.scss │ │ │ └── product-show.scss │ │ └── theme-global.scss │ ├── order │ │ ├── show.jsx │ │ ├── address.js │ │ ├── line-item.jsx │ │ ├── list.jsx │ │ ├── panel-view.jsx │ │ ├── summary.jsx │ │ └── shipment.jsx │ ├── taxon-filters │ │ ├── taxon.jsx │ │ ├── filter-bar.jsx │ │ └── styles │ │ │ └── filter-bar.scss │ ├── shared │ │ ├── header │ │ │ ├── brand-header.jsx │ │ │ ├── styles │ │ │ │ ├── search-form.scss │ │ │ │ └── header-styles.scss │ │ │ ├── locale-selector.jsx │ │ │ └── search-form.jsx │ │ ├── modal.jsx │ │ ├── loader.jsx │ │ ├── footer.jsx │ │ ├── infinite-scroller.jsx │ │ └── styles │ │ │ └── header.scss │ ├── store-navigation.jsx │ ├── flash.jsx │ ├── main.jsx │ ├── product │ │ ├── image-preview.jsx │ │ ├── thumbnail-list.jsx │ │ ├── image-thumbnail.jsx │ │ ├── properties.jsx │ │ ├── variants-list.jsx │ │ └── image-viewer.jsx │ ├── cart │ │ ├── styles │ │ │ └── notification-info.scss │ │ └── notification-info.jsx │ ├── user-form.jsx │ ├── layout.jsx │ ├── checkout-steps │ │ ├── shared │ │ │ └── form-field.jsx │ │ ├── address │ │ │ └── country-field.jsx │ │ ├── payment │ │ │ └── card-fields.jsx │ │ ├── checkout-success-page.jsx │ │ ├── delivery │ │ │ └── shipment.jsx │ │ ├── base-checkout-layout.jsx │ │ ├── delivery-form.jsx │ │ ├── confirmation-form.jsx │ │ └── payment-form.jsx │ ├── home-page.jsx │ ├── home-slider.jsx │ ├── product-list.jsx │ ├── product-tile.jsx │ ├── user-login.jsx │ └── user-signup.jsx ├── browser-history.jsx ├── actions │ ├── taxons.js │ ├── countries.js │ ├── loader.js │ ├── locale.js │ ├── order-list.js │ ├── utils.js │ ├── placed-order.js │ ├── flash.js │ ├── user.js │ ├── products.js │ ├── index.js │ └── checkout.js ├── apis │ ├── country.js │ ├── state.js │ ├── ams-adapters │ │ ├── spree-api-line-item-adapter.js │ │ ├── spree-api-taxon-adapter.js │ │ ├── spree-api-product-adapter.js │ │ └── spree-api-order-adapter.js │ ├── taxons.js │ ├── common-api-methods.js │ ├── user.js │ ├── checkout.js │ ├── products.js │ ├── line-item.js │ └── order.js ├── services │ ├── taxon-finder.js │ ├── order-finder.js │ ├── url-sanitizer.js │ ├── url-parser.js │ ├── error-message-formatter.jsx │ ├── local-storage-api.js │ ├── product-model.js │ └── checkout-step-calculator.js ├── reducers │ ├── taxons.js │ ├── country-list.js │ ├── user.js │ ├── display-loader.js │ ├── locale.js │ ├── flash.js │ ├── current-checkout-step.js │ ├── placed-order.js │ ├── order-list.js │ ├── index.js │ ├── product-list.js │ └── order.js ├── containers │ ├── order │ │ ├── show-connector.js │ │ ├── summary-connector.js │ │ └── list-connector.js │ ├── locale-selector-connector.js │ ├── flash-connector.js │ ├── cart │ │ ├── notification-info-connector.js │ │ └── cart-show-connector.js │ ├── user-signup-connector.js │ ├── user-login-connector.js │ ├── product-tile-connector.js │ ├── checkout-steps │ │ ├── address-fields-connector.js │ │ ├── checkout-success-connector.js │ │ ├── delivery-form-connector.js │ │ ├── confirmation-form-connector.js │ │ ├── payment-form-connector.jsx │ │ └── address-form-connector.js │ ├── header-connector.js │ ├── taxon-filters │ │ └── filter-bar-connector.js │ ├── search-form-connector.js │ ├── product │ │ └── product-show-connector.js │ └── home-page-connector.js ├── errors │ ├── invalid-checkout-step.js │ └── invalid-order-transition.js ├── index.js ├── store.js ├── connected-intl-provider.jsx └── routes.jsx ├── .gitignore ├── scripts ├── test.js └── translate.js ├── LICENSE.txt ├── package.json └── locales ├── en └── en.json └── es └── es.json /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /config/jest/CSSStub.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /config/jest/FileStub.js: -------------------------------------------------------------------------------- 1 | module.exports = "test-file-stub"; 2 | -------------------------------------------------------------------------------- /static.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": "build/", 3 | "routes": { 4 | "/**": "index.html" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinsol-spree-contrib/spree-on-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/b1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinsol-spree-contrib/spree-on-react/HEAD/src/images/b1.jpg -------------------------------------------------------------------------------- /src/images/b2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinsol-spree-contrib/spree-on-react/HEAD/src/images/b2.jpg -------------------------------------------------------------------------------- /src/images/bodybg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinsol-spree-contrib/spree-on-react/HEAD/src/images/bodybg.png -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinsol-spree-contrib/spree-on-react/HEAD/src/images/logo.png -------------------------------------------------------------------------------- /src/images/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vinsol-spree-contrib/spree-on-react/HEAD/src/images/logo2.png -------------------------------------------------------------------------------- /src/constants/app-defaults.js: -------------------------------------------------------------------------------- 1 | const APP_DEFAULTS = { 2 | perPage: 5 3 | }; 4 | 5 | export default APP_DEFAULTS; 6 | -------------------------------------------------------------------------------- /src/constants/supported-locales.js: -------------------------------------------------------------------------------- 1 | const SUPPORTED_LOCALES = { 2 | "en-IN": "English", 3 | "es": "Spanish" 4 | }; 5 | 6 | export default SUPPORTED_LOCALES; 7 | -------------------------------------------------------------------------------- /src/components/styles/core/fonts.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Oswald:300,400,500,600,700'); 2 | @import url('https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,500,500i,700,700i'); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | translations 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /src/browser-history.jsx: -------------------------------------------------------------------------------- 1 | import createHistory from 'history/createBrowserHistory'; 2 | 3 | const history = createHistory(); 4 | /* 5 | This is the history object used to initialize store and also supplied into 6 | the router component. 7 | */ 8 | export default history; 9 | -------------------------------------------------------------------------------- /src/actions/taxons.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const Taxons = { 4 | addTaxons: (taxons) => { 5 | return { 6 | type: APP_ACTIONS.ADD_TAXONS, 7 | payload: taxons 8 | } 9 | } 10 | }; 11 | 12 | export default Taxons; 13 | -------------------------------------------------------------------------------- /src/components/order/show.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class OrderShow extends Component { 4 | render() { 5 | return ( 6 |
7 |
8 | ); 9 | }; 10 | }; 11 | 12 | export default OrderShow; 13 | -------------------------------------------------------------------------------- /src/actions/countries.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const countries = { 4 | addCountries: (countriesResponse) => { 5 | return { 6 | type: APP_ACTIONS.ADD_COUNTRIES, 7 | payload: countriesResponse 8 | }; 9 | } 10 | }; 11 | 12 | export default countries; 13 | -------------------------------------------------------------------------------- /src/components/styles/core/variables.scss: -------------------------------------------------------------------------------- 1 | $color-white: #fff; 2 | $color-black: #000; 3 | $link-color: #4eaf79; 4 | $border-grey: #E1E1E1; 5 | $light-grey-bg: #f7f7f9; 6 | $color-error-red: #d91604; 7 | $dark-grey-text : #2b2b2b; 8 | 9 | $roboto: 'Roboto', sans-serif; 10 | $oswald: 'Oswald', sans-serif; 11 | 12 | $full: 100%; 13 | -------------------------------------------------------------------------------- /src/apis/country.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | 3 | const CountryAPI = { 4 | getList: () => { 5 | return request 6 | .get(`${process.env.REACT_APP_API_BASE}/countries`) 7 | .query({ per_page: 300 }) 8 | .set('Accept', 'application/json') 9 | .send(); 10 | } 11 | } 12 | 13 | export default CountryAPI; 14 | -------------------------------------------------------------------------------- /src/services/taxon-finder.js: -------------------------------------------------------------------------------- 1 | const TaxonFinder = { 2 | findByPermalink: (permalink, taxons = []) => { 3 | let matchingTaxon; 4 | 5 | matchingTaxon = taxons.find((taxon) => { 6 | return (`/t/${ taxon.permalink }` === permalink); 7 | }); 8 | 9 | return matchingTaxon; 10 | } 11 | } 12 | 13 | export default TaxonFinder; 14 | -------------------------------------------------------------------------------- /src/services/order-finder.js: -------------------------------------------------------------------------------- 1 | const OrderFinder = { 2 | find: (orderId, orders = []) => { 3 | const radix = 10; 4 | let order; 5 | 6 | order = orders.find((order) => { 7 | return (parseInt(order.id, radix) === parseInt(orderId, radix)); 8 | }); 9 | 10 | return order; 11 | } 12 | } 13 | 14 | export default OrderFinder; 15 | -------------------------------------------------------------------------------- /src/actions/loader.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const loader = { 4 | displayLoader: () => { 5 | return { 6 | type: APP_ACTIONS.DISPLAY_LOADER, 7 | }; 8 | }, 9 | 10 | hideLoader: () => { 11 | return { 12 | type: APP_ACTIONS.HIDE_LOADER, 13 | } 14 | } 15 | }; 16 | 17 | export default loader; 18 | -------------------------------------------------------------------------------- /src/apis/state.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | 3 | const StateAPI = { 4 | getByCountry: (countryId) => { 5 | return request 6 | .get(`${process.env.REACT_APP_API_BASE}/states`) 7 | .query({ per_page: 300, country_id: countryId }) 8 | .set('Accept', 'application/json') 9 | .send(); 10 | } 11 | } 12 | 13 | export default StateAPI; 14 | -------------------------------------------------------------------------------- /src/reducers/taxons.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = []; 4 | 5 | const taxons = function(state = initialState, action) { 6 | switch (action.type) { 7 | case APP_ACTIONS.ADD_TAXONS: 8 | return Object.assign( [], action.payload); 9 | default: 10 | return state; 11 | } 12 | } 13 | 14 | export default taxons; 15 | -------------------------------------------------------------------------------- /src/services/url-sanitizer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Returns the URL unchanged if it is an absolute URL 3 | Else, appends with REACT_APP_API_HOST. 4 | */ 5 | const URLSanitizer = { 6 | makeAbsolute: (url) => { 7 | if (url.indexOf('http') !== -1) { 8 | return url; 9 | } 10 | else { 11 | return `${ process.env.REACT_APP_API_HOST }${ url }`; 12 | } 13 | } 14 | }; 15 | 16 | export default URLSanitizer; 17 | -------------------------------------------------------------------------------- /src/actions/locale.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | import localStorageAPI from '../services/local-storage-api'; 3 | 4 | const locale = { 5 | setLocale: (locale) => { 6 | return (dispatch, getState) => { 7 | dispatch({ type: APP_ACTIONS.SET_LOCALE, payload: { locale: locale } }); 8 | 9 | localStorageAPI.save(getState()); 10 | } 11 | } 12 | }; 13 | 14 | export default locale; 15 | -------------------------------------------------------------------------------- /src/containers/order/show-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import OrderShow from '../../components/order/show'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return {}; 7 | }; 8 | 9 | const mapDispatchToProps = (dispatch) => { 10 | return {}; 11 | }; 12 | 13 | const OrderShowConnector = connect(mapStateToProps, mapDispatchToProps)(OrderShow); 14 | 15 | export default OrderShowConnector; 16 | -------------------------------------------------------------------------------- /src/reducers/country-list.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = { 4 | countries: [] 5 | }; 6 | 7 | const countryList = function(state = initialState, action) { 8 | switch (action.type) { 9 | case APP_ACTIONS.ADD_COUNTRIES: 10 | return Object.assign({}, state, action.payload); 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | export default countryList; 17 | -------------------------------------------------------------------------------- /src/actions/order-list.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const orderList = { 4 | addOrders: (ordersResponse) => { 5 | return { 6 | type: APP_ACTIONS.ADD_ORDERS, 7 | payload: ordersResponse 8 | }; 9 | }, 10 | 11 | addOrder: (order) => { 12 | return { 13 | type: APP_ACTIONS.ADD_ORDER, 14 | payload: order 15 | } 16 | } 17 | }; 18 | 19 | export default orderList; 20 | -------------------------------------------------------------------------------- /src/reducers/user.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = {}; 4 | 5 | const taxons = function(state = initialState, action) { 6 | switch (action.type) { 7 | case APP_ACTIONS.LOGIN: 8 | return Object.assign( {}, action.payload); 9 | case APP_ACTIONS.LOGOUT: 10 | return initialState; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | export default taxons; 17 | -------------------------------------------------------------------------------- /src/reducers/display-loader.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = true; 4 | 5 | const displayLoader = function(state = initialState, action) { 6 | switch (action.type) { 7 | case APP_ACTIONS.DISPLAY_LOADER: 8 | return true; 9 | case APP_ACTIONS.HIDE_LOADER: 10 | return false; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | export default displayLoader; 17 | -------------------------------------------------------------------------------- /src/actions/utils.js: -------------------------------------------------------------------------------- 1 | const Utils = { 2 | tokenForAPI: (userToken, orderToken) => { 3 | let tokenParam = {}; 4 | 5 | if ( userToken ) { 6 | tokenParam = { token: userToken }; 7 | } 8 | else { 9 | if ( orderToken ) { 10 | tokenParam = { order_token: orderToken }; 11 | } 12 | } 13 | 14 | return tokenParam; 15 | } 16 | } 17 | 18 | const tokenForAPI = Utils.tokenForAPI; 19 | 20 | export { tokenForAPI }; 21 | -------------------------------------------------------------------------------- /src/reducers/locale.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = { 4 | currentLocale: 'en', 5 | messages: {} 6 | }; 7 | 8 | const locale = function(state = initialState, action) { 9 | switch (action.type) { 10 | case APP_ACTIONS.SET_LOCALE: 11 | return Object.assign({}, state, { currentLocale: action.payload.locale }); 12 | default: 13 | return state; 14 | } 15 | } 16 | 17 | export default locale; 18 | -------------------------------------------------------------------------------- /src/constants/app-routes.js: -------------------------------------------------------------------------------- 1 | const APP_ROUTES = { 2 | homePageRoute: '/', 3 | searchPageRoute: '/search/:searchTerm', 4 | cartPageRoute: '/cart', 5 | ordersPageRoute: '/orders', 6 | checkout: { 7 | addressPageRoute: '/checkout/address', 8 | deliveryPageRoute: '/checkout/delivery', 9 | paymentPageRoute: '/checkout/payment', 10 | confirmPageRoute: '/checkout/confirm', 11 | completePageRoute: '/checkout/complete' 12 | } 13 | }; 14 | 15 | export default APP_ROUTES; 16 | -------------------------------------------------------------------------------- /src/reducers/flash.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = { 4 | message: null, 5 | type: null, 6 | visible: false 7 | }; 8 | 9 | const flash = function(state = initialState, action) { 10 | switch (action.type) { 11 | case APP_ACTIONS.SET_FLASH: 12 | return action.payload; 13 | case APP_ACTIONS.HIDE_FLASH: 14 | return initialState; 15 | default: 16 | return state; 17 | } 18 | } 19 | 20 | export default flash; 21 | -------------------------------------------------------------------------------- /src/reducers/current-checkout-step.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = ""; 4 | 5 | const currentCheckoutStep = function(state = initialState, action) { 6 | switch (action.type) { 7 | case APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP: 8 | return action.payload; 9 | case APP_ACTIONS.CLEAR_CURRENT_CHECKOUT_STEP: 10 | return initialState; 11 | default: 12 | return state; 13 | } 14 | } 15 | 16 | export default currentCheckoutStep; 17 | -------------------------------------------------------------------------------- /src/components/taxon-filters/taxon.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { MenuItem } from 'react-bootstrap'; 3 | 4 | class Taxon extends Component { 5 | 6 | handleTaxonClick (taxon) { 7 | this.props.handleTaxonClick(this.props.taxon.permalink); 8 | }; 9 | 10 | render() { 11 | return ( 12 | 13 | { this.props.taxon.name } 14 | 15 | ) 16 | } 17 | } 18 | 19 | export default Taxon; 20 | -------------------------------------------------------------------------------- /src/components/shared/header/brand-header.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import APP_ROUTES from '../../../constants/app-routes'; 5 | import styles from './styles/header-styles.scss'; 6 | 7 | class BrandHeader extends Component { 8 | render() { 9 | return ( 10 | 11 | Spree On React 12 | 13 | ); 14 | } 15 | } 16 | 17 | export default BrandHeader; 18 | -------------------------------------------------------------------------------- /src/components/styles/components/mobile-nav.scss: -------------------------------------------------------------------------------- 1 | .header-menu-dropdown { 2 | width: $full; 3 | height: $full; 4 | position: fixed; 5 | top: 0; 6 | left: 0; 7 | z-index: 999; 8 | background: rgba(0, 0, 0, .8); 9 | 10 | .header-menu-close { 11 | position: absolute; 12 | top: 10px; 13 | right: 10px; 14 | color: $color-white; 15 | font-size: 24px; 16 | } 17 | 18 | .header-menu-holder { 19 | width: 88%; 20 | height: $full; 21 | position: relative; 22 | background: $color-white; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/store-navigation.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import NavLinks from './navigation-link' 3 | import SearchModalConnector from '../containers/search-modal-connector'; 4 | 5 | class StoreNavigation extends Component { 6 | render () { 7 | return ( 8 |
9 | 13 |
14 | ) 15 | } 16 | } 17 | 18 | export default StoreNavigation; 19 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | process.env.PUBLIC_URL = ''; 3 | 4 | // Load environment variables from .env file. Suppress warnings using silent 5 | // if this file is missing. dotenv will never modify any environment variables 6 | // that have already been set. 7 | // https://github.com/motdotla/dotenv 8 | require('dotenv').config({silent: true}); 9 | 10 | const jest = require('jest'); 11 | const argv = process.argv.slice(2); 12 | 13 | // Watch unless on CI 14 | if (!process.env.CI) { 15 | argv.push('--watch'); 16 | } 17 | 18 | 19 | jest.run(argv); 20 | -------------------------------------------------------------------------------- /src/apis/ams-adapters/spree-api-line-item-adapter.js: -------------------------------------------------------------------------------- 1 | const SpreeAPILineItemAdapter = { 2 | processItem: (lineItemAMS) => { 3 | SpreeAPILineItemAdapter._process(lineItemAMS); 4 | 5 | return lineItemAMS.line_item; 6 | }, 7 | 8 | /* 9 | PRIVATE METHODS 10 | */ 11 | _process: (lineItemAMS) => { 12 | lineItemAMS.line_item.variant = lineItemAMS.variants[0]; 13 | if (lineItemAMS.line_item.variant) { 14 | lineItemAMS.line_item.variant.images = lineItemAMS.images; 15 | } 16 | } 17 | 18 | }; 19 | 20 | export default SpreeAPILineItemAdapter; 21 | -------------------------------------------------------------------------------- /src/services/url-parser.js: -------------------------------------------------------------------------------- 1 | const UrlParser = { 2 | 3 | getQueryVariable: (variable) => { 4 | let query = window.location.pathname; 5 | let vars = query.split('/search/'); 6 | return vars[1]; 7 | // TODO: Trying out url params as opposed to query params for search words. 8 | // for (let i = 0; i < vars.length; i++) { 9 | // let pair = vars[i].split('='); 10 | // if (decodeURIComponent(pair[0]) === variable) { 11 | // return decodeURIComponent(pair[1]); 12 | // } 13 | // } 14 | } 15 | } 16 | 17 | export default UrlParser; 18 | -------------------------------------------------------------------------------- /src/apis/taxons.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | import SpreeAPITaxonAdapter from './ams-adapters/spree-api-taxon-adapter'; 3 | 4 | const TaxonAPI = { 5 | getList: () => { 6 | return request 7 | .get(`${ process.env.REACT_APP_AMS_API_BASE }/taxons`) 8 | .set('Accept', 'application/json') 9 | .then((response) => { 10 | let processedResponse = SpreeAPITaxonAdapter.processList(response.body); 11 | response.body = processedResponse; 12 | 13 | return response; 14 | }); 15 | } 16 | }; 17 | 18 | export default TaxonAPI; 19 | -------------------------------------------------------------------------------- /src/reducers/placed-order.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = { 4 | shipments: [], 5 | checkout_steps: [], 6 | state: 'complete' 7 | }; 8 | 9 | const placedOrder = function(state = initialState, action) { 10 | switch (action.type) { 11 | case APP_ACTIONS.ADD_PLACED_ORDER: 12 | return Object.assign({}, action.payload); 13 | case APP_ACTIONS.DESTROY_PLACED_ORDER: 14 | return Object.assign({}, initialState); 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export default placedOrder; 21 | -------------------------------------------------------------------------------- /src/apis/common-api-methods.js: -------------------------------------------------------------------------------- 1 | const CommonAPIMethods = { 2 | 3 | /* If orderToken is present in params, set order_token in query. 4 | If api_token is present in params, set token. 5 | */ 6 | getTokenParams: (params) => { 7 | let orderToken = params.order_token || params.orderToken; 8 | 9 | if (orderToken) { 10 | return { order_token: orderToken }; 11 | } 12 | else if (params.api_token) { 13 | return { token: params.api_token }; 14 | } 15 | else { 16 | return {}; 17 | } 18 | } 19 | }; 20 | 21 | export default CommonAPIMethods; 22 | -------------------------------------------------------------------------------- /src/components/flash.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Alert } from 'react-bootstrap'; 3 | 4 | class Flash extends Component { 5 | 6 | render() { 7 | let flashDiv = null; 8 | if (this.props.flash.visible) { 9 | flashDiv = 10 | { this.props.flash.message } 11 | ; 12 | } 13 | 14 | return ( 15 |
{ flashDiv }
16 | ); 17 | }; 18 | 19 | }; 20 | 21 | export default Flash; 22 | -------------------------------------------------------------------------------- /src/containers/order/summary-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import OrderSummary from '../../components/order/summary'; 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | order: state.order, 8 | placedOrder: ownProps.placedOrder, 9 | currentCheckoutStep: state.currentCheckoutStep 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | return { 15 | 16 | }; 17 | }; 18 | 19 | const OrderSummaryConnector = connect(mapStateToProps, mapDispatchToProps)(OrderSummary); 20 | 21 | export default OrderSummaryConnector; 22 | -------------------------------------------------------------------------------- /src/components/shared/modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class Modal extends Component { 4 | render() { 5 | let showModalClass = this.props.showModal ? 'show-modal' : ' '; 6 | 7 | return ( 8 |
9 | 10 |
11 | { this.props.children } 12 |
13 |
14 | ); 15 | } 16 | } 17 | 18 | export default Modal; 19 | -------------------------------------------------------------------------------- /src/components/shared/loader.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import loaderImage from '../../images/loader.svg'; 3 | 4 | class Loader extends Component { 5 | render() { 6 | let loaderDiv = null; 7 | 8 | if (this.props.displayLoader) { 9 | loaderDiv =
10 |
11 | Loader 12 |
13 |
; 14 | } 15 | 16 | return ( 17 | loaderDiv 18 | ); 19 | } 20 | } 21 | 22 | export default Loader; 23 | -------------------------------------------------------------------------------- /src/services/error-message-formatter.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ErrorMessageFormatter = { 4 | 5 | formatFormSubmissionErrors: (errors) => { 6 | const title = 'Please fix the below errors.'; 7 | let errorMessageList = errors.map((error, idx) => { 8 | return ( 9 |
  • 10 | { error } 11 |
  • 12 | ); 13 | }); 14 | 15 | return ( 16 |
    17 |

    { title }

    18 | 19 | 22 |
    23 | ); 24 | } 25 | }; 26 | 27 | export default ErrorMessageFormatter; 28 | -------------------------------------------------------------------------------- /src/actions/placed-order.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | import localStorageAPI from '../services/local-storage-api'; 3 | 4 | const placedOrder = { 5 | addPlacedOrder: (order) => { 6 | return (dispatch, getState) => { 7 | dispatch ({ type: APP_ACTIONS.ADD_PLACED_ORDER, payload: order }); 8 | localStorageAPI.save(getState()); 9 | } 10 | }, 11 | 12 | clearPlacedOrder: () => { 13 | return (dispatch, getState) => { 14 | dispatch( { type: APP_ACTIONS.DESTROY_PLACED_ORDER } ); 15 | localStorageAPI.save(getState()); 16 | }; 17 | } 18 | }; 19 | 20 | export default placedOrder; 21 | -------------------------------------------------------------------------------- /src/components/styles/components/footer.scss: -------------------------------------------------------------------------------- 1 | .footer-section { 2 | width: $full; 3 | height: 60px; 4 | padding-top: 15px; 5 | display: table; 6 | position: absolute; 7 | left: 0; 8 | bottom: 0; 9 | color: $color-white; 10 | font-weight: 300; 11 | background: $color-black; 12 | 13 | .footer-left-content { 14 | padding-top: 5px; 15 | color: $color-white; 16 | 17 | a { 18 | color: $link-color; 19 | } 20 | } 21 | 22 | .footer-right-content { 23 | text-align: right; 24 | 25 | a { 26 | color: $link-color; 27 | } 28 | 29 | .user-link-block { 30 | color: $color-white; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /src/errors/invalid-checkout-step.js: -------------------------------------------------------------------------------- 1 | function InvalidCheckoutStepException(message) { 2 | this.message = message; 3 | // Use V8's native method if available, otherwise fallback 4 | if ("captureStackTrace" in Error) 5 | Error.captureStackTrace(this, InvalidCheckoutStepException); 6 | else 7 | this.stack = (new Error()).stack; 8 | }; 9 | 10 | InvalidCheckoutStepException.prototype = Object.create(Error.prototype); 11 | InvalidCheckoutStepException.prototype.name = "InvalidCheckoutStepException"; 12 | InvalidCheckoutStepException.prototype.constructor = InvalidCheckoutStepException; 13 | 14 | export default InvalidCheckoutStepException; 15 | -------------------------------------------------------------------------------- /src/containers/locale-selector-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Actions from '../actions'; 4 | import LocaleSelector from '../components/shared/header/locale-selector'; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | return { 8 | currentLocale: state.locale.currentLocale 9 | }; 10 | }; 11 | 12 | const mapDispatchToProps = (dispatch) => { 13 | return { 14 | setLocale: (locale) => { 15 | dispatch(Actions.setLocale(locale)); 16 | } 17 | }; 18 | }; 19 | 20 | const LocaleSelectorConnector = connect(mapStateToProps, mapDispatchToProps)(LocaleSelector); 21 | 22 | export default LocaleSelectorConnector; 23 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/errors/invalid-order-transition.js: -------------------------------------------------------------------------------- 1 | function InvalidOrderTransitionException(message) { 2 | this.message = message; 3 | // Use V8's native method if available, otherwise fallback 4 | if ("captureStackTrace" in Error) 5 | Error.captureStackTrace(this, InvalidOrderTransitionException); 6 | else 7 | this.stack = (new Error()).stack; 8 | }; 9 | 10 | InvalidOrderTransitionException.prototype = Object.create(Error.prototype); 11 | InvalidOrderTransitionException.prototype.name = "InvalidOrderTransitionException"; 12 | InvalidOrderTransitionException.prototype.constructor = InvalidOrderTransitionException; 13 | 14 | export default InvalidOrderTransitionException; 15 | -------------------------------------------------------------------------------- /src/containers/flash-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Actions from '../actions'; 4 | import Flash from '../components/flash'; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | let flash = { 8 | flash: { 9 | message: state.flash.message, 10 | type: state.flash.type, 11 | visible: state.flash.visible 12 | } 13 | } 14 | return flash 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | handleAlertDismiss: () => { 20 | dispatch (Actions.hideFlash()) 21 | } 22 | }; 23 | }; 24 | 25 | const FlashConnector = connect(mapStateToProps, mapDispatchToProps)(Flash); 26 | 27 | export default FlashConnector; 28 | -------------------------------------------------------------------------------- /src/services/local-storage-api.js: -------------------------------------------------------------------------------- 1 | const localStorageAPI = { 2 | save: (payload) => { 3 | try { 4 | localStorage.setItem('storeState', JSON.stringify(payload)); 5 | } catch (err) { /* Silently ignore */} 6 | }, 7 | 8 | load: () => { 9 | try { 10 | const serializedState = localStorage.getItem('storeState'); 11 | 12 | if (serializedState == null) { 13 | return undefined; 14 | } 15 | 16 | return JSON.parse(serializedState); 17 | } catch (err) { 18 | return undefined; 19 | } 20 | }, 21 | 22 | clear: () => { 23 | try { 24 | localStorage.clear(); 25 | } catch (err) { /* Silently ignore */} 26 | } 27 | }; 28 | 29 | export default localStorageAPI; 30 | -------------------------------------------------------------------------------- /src/containers/cart/notification-info-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import CartNotificationInfo from '../../components/cart/notification-info'; 4 | 5 | /* 6 | Pass line items only if order is not complete. 7 | */ 8 | const mapStateToProps = (state, ownProps) => { 9 | let lineItems = []; 10 | if (state.order.state !== 'complete') { 11 | lineItems = state.order.line_items; 12 | } 13 | 14 | return { 15 | lineItems: lineItems 16 | }; 17 | }; 18 | 19 | const mapDispatchToProps = (dispatch) => { 20 | return {}; 21 | }; 22 | 23 | const CartNotificationInfoConnector = connect(mapStateToProps, mapDispatchToProps)(CartNotificationInfo); 24 | 25 | export default CartNotificationInfoConnector; 26 | -------------------------------------------------------------------------------- /src/components/main.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { ConnectedRouter as Router } from 'react-router-redux'; 4 | 5 | import spreeStore from '../store'; 6 | import history from '../browser-history'; 7 | import configRoutes from '../routes'; 8 | import ConnectedIntlProvider from '../connected-intl-provider'; 9 | 10 | class Main extends Component { 11 | render() { 12 | return ( 13 | 14 | 15 | 16 | { configRoutes() } 17 | 18 | 19 | 20 | ); 21 | } 22 | } 23 | 24 | export default Main; 25 | -------------------------------------------------------------------------------- /src/components/shared/footer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LocaleSelectorConnector from '../../containers/locale-selector-connector'; 3 | 4 | class Footer extends Component { 5 | render() { 6 | return ( 7 | 22 | ); 23 | } 24 | } 25 | 26 | export default Footer; 27 | -------------------------------------------------------------------------------- /src/components/product/image-preview.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import URLSanitizer from '../../services/url-sanitizer'; 4 | 5 | class ProductImagePreview extends Component { 6 | 7 | render() { 8 | let imageUrl = URLSanitizer.makeAbsolute(this.props.productImage.large_url); 9 | 10 | return ( 11 |
    12 |
    13 | 14 | {'productName'} 18 | 19 | 20 |
    21 |
    22 | ); 23 | }; 24 | }; 25 | 26 | export default ProductImagePreview; 27 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { addLocaleData } from 'react-intl'; 4 | 5 | /* 6 | Load locales that need to be supported 7 | */ 8 | import en from 'react-intl/locale-data/en'; 9 | import es from 'react-intl/locale-data/es'; 10 | 11 | import Main from './components/main'; 12 | 13 | /* 14 | We are still loading bootstrap via CSS directly. Not using css-modules here. 15 | */ 16 | import styles from 'bootstrap/dist/css/bootstrap.css'; 17 | import bootstrapTheme from 'bootstrap/dist/css/bootstrap-theme.css'; 18 | 19 | /* 20 | These are non css-modules styles. 21 | */ 22 | import './components/styles/theme-global.scss'; 23 | 24 | addLocaleData([...en, ...es]); 25 | 26 | ReactDOM.render( 27 |
    28 | , 29 | document.getElementById('root') 30 | ); 31 | -------------------------------------------------------------------------------- /src/actions/flash.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const flash = { 4 | setFlash: (message, type) => { 5 | return { 6 | type: APP_ACTIONS.SET_FLASH, 7 | payload: { 8 | message, 9 | type, 10 | visible: true 11 | } 12 | } 13 | }, 14 | 15 | hideFlash: () => { 16 | return { 17 | type: APP_ACTIONS.HIDE_FLASH 18 | } 19 | }, 20 | 21 | /* This method displays the flash message for +timeoutInMillis+ 22 | and then hides it. 23 | */ 24 | showFlash: (message, type = 'success', timeoutInMillis = 5000) => { 25 | return (dispatch, getState) => { 26 | dispatch (flash.setFlash(message, type)); 27 | 28 | setTimeout( () => { 29 | dispatch (flash.hideFlash()) 30 | }, 31 | timeoutInMillis ); 32 | } 33 | } 34 | }; 35 | 36 | export default flash; 37 | -------------------------------------------------------------------------------- /src/components/cart/styles/notification-info.scss: -------------------------------------------------------------------------------- 1 | .headerCartBlock { 2 | margin-left: 20px; 3 | display: inline-block; 4 | 5 | .headerCartLink { 6 | padding-left: 10px; 7 | position: relative; 8 | font-family: 'Roboto', sans-serif; 9 | font-size: 12px; 10 | } 11 | 12 | .headerCartIcon { 13 | margin-right: 2px; 14 | } 15 | 16 | .headerCartCount { 17 | min-width: 1; 18 | margin-left: 5px; 19 | padding: 0; 20 | display: inline-block; 21 | color: #cacaca; 22 | font-size: 10px; 23 | font-weight: 300; 24 | background: none; 25 | } 26 | 27 | @media (max-width: 767px) { 28 | margin-left: 0; 29 | padding-top: 5px; 30 | 31 | .headerCartLink { 32 | font-size: 16px; 33 | } 34 | 35 | .headerCartLink { 36 | padding-left: 0; 37 | } 38 | 39 | .headerCartCount { 40 | margin-left: 0; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/reducers/order-list.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | import OrderFinder from '../services/order-finder'; 3 | 4 | const initialState = { 5 | orders: [] 6 | }; 7 | 8 | const orderList = function(state = initialState, action) { 9 | let newOrderList, orderInList; 10 | 11 | switch (action.type) { 12 | case APP_ACTIONS.ADD_ORDERS: 13 | return Object.assign({}, state, action.payload); 14 | 15 | case APP_ACTIONS.ADD_ORDER: 16 | orderInList = OrderFinder.find(action.payload.id, state.orders); 17 | 18 | if (orderInList) { 19 | return state; 20 | } 21 | else { 22 | newOrderList = Object.assign( [], state.orders ); 23 | newOrderList.push(action.payload); 24 | return Object.assign ( {}, state, { orders: newOrderList } ); 25 | } 26 | 27 | default: 28 | return state; 29 | } 30 | } 31 | 32 | export default orderList; 33 | -------------------------------------------------------------------------------- /src/components/product/thumbnail-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ProductImageThumb from './image-thumbnail'; 3 | 4 | class ThumbnailList extends Component { 5 | render() { 6 | let imagesMarkup; 7 | 8 | imagesMarkup = this.props.images.map((image, idx) => { 9 | return ( 10 | 15 | ); 16 | }); 17 | 18 | return ( 19 |
    20 | { imagesMarkup } 21 |
    22 | ); 23 | }; 24 | }; 25 | 26 | export default ThumbnailList; 27 | -------------------------------------------------------------------------------- /scripts/translate.js: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import {sync as globSync} from 'glob'; 3 | import {sync as mkdirpSync} from 'mkdirp'; 4 | import deepMerge from 'deepmerge'; 5 | 6 | // This is where we keep the locale files. 7 | const LOCALE_FILES_PATTERN = './locales/**/*.json'; 8 | // This is the directory where consolidated JSON is stored. 9 | const TRANSLATION_FILE_DIR = './translations/'; 10 | const TRANSLATION_FILE_NAME = 'app-translations.json'; 11 | var appTranslations = {}; 12 | 13 | // Deep Merging all locale files to build one consolidated JSON. 14 | globSync(LOCALE_FILES_PATTERN) 15 | .map((filename) => fs.readFileSync(filename, 'utf8')) 16 | .forEach((file) => { 17 | appTranslations = deepMerge(appTranslations, JSON.parse(file)); 18 | }); 19 | 20 | mkdirpSync(TRANSLATION_FILE_DIR); 21 | 22 | fs.writeFileSync(TRANSLATION_FILE_DIR + TRANSLATION_FILE_NAME, JSON.stringify(appTranslations, null, 2) ); 23 | -------------------------------------------------------------------------------- /src/components/user-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | class userFormBlock extends Component { 5 | 6 | render() { 7 | return ( 8 |
    9 | 10 | 11 |
    12 |
    13 |
    14 | 18 |
    19 | { this.props.children } 20 |
    21 |
    22 | 23 |
    24 | ); 25 | } 26 | } 27 | 28 | export default userFormBlock; 29 | -------------------------------------------------------------------------------- /src/components/cart/notification-info.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import styles from './styles/notification-info.scss'; 4 | 5 | import APP_ROUTES from '../../constants/app-routes'; 6 | 7 | class CartNotificationInfo extends Component { 8 | render() { 9 | //
    10 | return ( 11 |
    12 | 13 | 14 | Cart 15 | { this.props.lineItems.length } 16 | 17 |
    18 | 19 | ); 20 | }; 21 | }; 22 | 23 | export default CartNotificationInfo; 24 | -------------------------------------------------------------------------------- /src/components/layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import HeaderConnector from '../containers/header-connector'; 3 | import FlashConnector from '../containers/flash-connector'; 4 | import Footer from './shared/footer'; 5 | 6 | class Layout extends Component { 7 | render() { 8 | // 12 | return ( 13 |
    14 | 15 | 16 |
    17 |
    18 | 19 |
    20 | { this.props.children } 21 |
    22 | 23 |
    24 |
    25 | ); 26 | } 27 | } 28 | 29 | export default Layout; 30 | -------------------------------------------------------------------------------- /src/components/product/image-thumbnail.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import URLSanitizer from '../../services/url-sanitizer'; 4 | 5 | class ProductImageThumb extends Component { 6 | 7 | render() { 8 | let imageUrl = URLSanitizer.makeAbsolute(this.props.imageUrl); 9 | 10 | return ( 11 |
    12 | 13 | {'productName'} this.props.onMouseOverThumbnail(this.props.imageNo)} 17 | onMouseOut={this.props.onMouseOutThumbnail} 18 | onClick={()=> this.props.onClickThumbnail(this.props.imageNo)}> 19 | 20 | 21 |
    22 | ); 23 | }; 24 | }; 25 | 26 | export default ProductImageThumb; 27 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import { reducer as formReducer } from 'redux-form'; 4 | 5 | import displayLoader from './display-loader'; 6 | import productList from './product-list'; 7 | import taxons from './taxons'; 8 | import order from './order'; 9 | import orderList from './order-list'; 10 | import flash from './flash'; 11 | import countryList from './country-list'; 12 | import currentCheckoutStep from './current-checkout-step'; 13 | import placedOrder from './placed-order'; 14 | import user from './user'; 15 | import locale from './locale'; 16 | 17 | const AppReducer = combineReducers({ 18 | displayLoader, 19 | productList, 20 | taxons, 21 | order, 22 | orderList, 23 | flash, 24 | countryList, 25 | currentCheckoutStep, 26 | placedOrder, 27 | user, 28 | locale, 29 | routing: routerReducer, 30 | form: formReducer 31 | }); 32 | 33 | export default AppReducer; 34 | -------------------------------------------------------------------------------- /src/components/shared/header/styles/search-form.scss: -------------------------------------------------------------------------------- 1 | .searchHolder { 2 | height: 40px; 3 | position: relative; 4 | 5 | .headerSearchLabel { 6 | width: 1px; 7 | height: 1px; 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | z-index: 0; 12 | } 13 | 14 | .headerSearchIcon { 15 | position: absolute; 16 | top: 14px; 17 | right: 10px; 18 | color: #000; 19 | font-size: 12px; 20 | } 21 | 22 | .headerSearchInput { 23 | width: 100%; 24 | height: 40px; 25 | border: solid 1px #C7C7C7; 26 | padding: 0 20px 0 10px; 27 | color: #000; 28 | font-family: 'Roboto', sans-serif; 29 | font-size: 12px; 30 | font-weight: 500; 31 | } 32 | 33 | @media (max-width: 767px) { 34 | .headerSearchIcon { 35 | top: 9px; 36 | right: 5px; 37 | } 38 | 39 | .headerSearchInput { 40 | height: 30px; 41 | padding: 0 15px 0 5px; 42 | font-size: 11px; 43 | line-height: 30px; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/apis/ams-adapters/spree-api-taxon-adapter.js: -------------------------------------------------------------------------------- 1 | const SpreeAPITaxonAdapter = { 2 | processList: (taxonsListAMS) => { 3 | let parentTaxons = taxonsListAMS.taxons.filter((taxon) => { 4 | return taxon.parent_id === null; 5 | }); 6 | 7 | parentTaxons.forEach((taxon) => { 8 | SpreeAPITaxonAdapter._process(taxon, taxonsListAMS); 9 | }); 10 | 11 | taxonsListAMS.taxons.forEach((product) => { 12 | SpreeAPITaxonAdapter._process(product, taxonsListAMS); 13 | }); 14 | 15 | return taxonsListAMS; 16 | }, 17 | 18 | /* 19 | PRIVATE METHODS 20 | */ 21 | _process: (taxon, taxonsListAMS) => { 22 | let childTaxons = taxonsListAMS.taxons.filter((_taxon) => { 23 | return _taxon.parent_id === taxon.id; 24 | }); 25 | 26 | taxon.taxons = childTaxons; 27 | 28 | childTaxons.forEach((innerTaxon) => { 29 | SpreeAPITaxonAdapter._process(innerTaxon, taxonsListAMS); 30 | }); 31 | } 32 | 33 | }; 34 | 35 | export default SpreeAPITaxonAdapter; 36 | -------------------------------------------------------------------------------- /src/components/checkout-steps/shared/form-field.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FormField = { 4 | inputFieldMarkup: ({ input, label, type, meta: { touched, error } }) => { 5 | let inputClassName, errorClassName; 6 | inputClassName = type === 'text' ? 'primary-input-field ' : ' '; 7 | 8 | if (touched && error) { 9 | errorClassName = "has-error "; 10 | } 11 | 12 | return( 13 |
    14 | 15 |
    16 | 21 | { touched && error && { error } } 22 |
    23 |
    24 | ); 25 | } 26 | 27 | }; 28 | 29 | export default FormField; 30 | -------------------------------------------------------------------------------- /src/actions/user.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | import localStorageAPI from '../services/local-storage-api'; 3 | import OrdersAPI from '../apis/order'; 4 | import Actions from './'; 5 | 6 | const user = { 7 | login: (userResponse) => { 8 | return (dispatch, getState) => { 9 | 10 | dispatch( { 11 | type: APP_ACTIONS.LOGIN, 12 | payload: userResponse 13 | }); 14 | 15 | dispatch (Actions.clearPlacedOrder()); 16 | OrdersAPI.getCurrent(userResponse.token).then((response) => { 17 | dispatch (Actions.updateOrderInState(response.body)); 18 | }); 19 | 20 | localStorageAPI.save(getState()); 21 | } 22 | }, 23 | 24 | logout: () => { 25 | return (dispatch, getState) => { 26 | dispatch ({ 27 | type: APP_ACTIONS.LOGOUT 28 | }); 29 | localStorageAPI.clear(); 30 | 31 | dispatch (Actions.clearPlacedOrder()); 32 | dispatch (Actions.clearOrder()); 33 | } 34 | } 35 | }; 36 | 37 | export default user; 38 | -------------------------------------------------------------------------------- /src/components/shared/header/locale-selector.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { DropdownButton, MenuItem } from 'react-bootstrap'; 3 | 4 | import SUPPORTED_LOCALES from '../../../constants/supported-locales'; 5 | // import styles from './styles/header-styles.scss'; 6 | 7 | class LocaleSelector extends Component { 8 | render() { 9 | const localeSelectorMarkup = Object.keys(SUPPORTED_LOCALES).map((localeKey, idx) => { 10 | return( 11 | { SUPPORTED_LOCALES[localeKey] } 12 | ); 13 | }); 14 | 15 | // TODO: Title 16 | return ( 17 |
    18 | 19 | { localeSelectorMarkup } 20 | 21 |
    22 | ); 23 | } 24 | } 25 | 26 | export default LocaleSelector; 27 | -------------------------------------------------------------------------------- /src/constants/app-actions.js: -------------------------------------------------------------------------------- 1 | const APP_ACTIONS = { 2 | DISPLAY_LOADER: 'DISPLAY_LOADER', 3 | HIDE_LOADER: 'HIDE_LOADER', 4 | 5 | ADD_PRODUCTS: 'ADD_PRODUCTS', 6 | APPEND_PRODUCTS: 'APPEND_PRODUCTS', 7 | ADD_PRODUCT: 'ADD_PRODUCT', 8 | ADD_PLACED_ORDER: 'ADD_PLACED_ORDER', 9 | DESTROY_PLACED_ORDER: 'DESTROY_PLACED_ORDER', 10 | 11 | ADD_ORDERS: 'ADD_ORDERS', 12 | ADD_ORDER: 'ADD_ORDER', 13 | 14 | ADD_TAXONS: 'ADD_TAXONS', 15 | 16 | CREATE_ORDER: 'CREATE_ORDER', 17 | ADD_PRODUCT_TO_CART: 'ADD_PRODUCT_TO_CART', 18 | DESTROY_ORDER: 'DESTROY_ORDER', 19 | REMOVE_LINE_ITEM: 'REMOVE_LINE_ITEM', 20 | UPDATE_LINE_ITEM: 'UPDATE_LINE_ITEM', 21 | 22 | SET_FLASH: 'SET_FLASH', 23 | HIDE_FLASH: 'HIDE_FLASH', 24 | 25 | ADD_COUNTRIES: 'ADD_COUNTRIES', 26 | 27 | SET_CURRENT_CHECKOUT_STEP: 'SET_CURRENT_CHECKOUT_STEP', 28 | CLEAR_CURRENT_CHECKOUT_STEP: 'CLEAR_CURRENT_CHECKOUT_STEP', 29 | 30 | SET_LOCALE: 'SET_LOCALE', 31 | 32 | LOGIN: 'LOGIN', 33 | LOGOUT: 'LOGOUT' 34 | }; 35 | 36 | export default APP_ACTIONS; 37 | -------------------------------------------------------------------------------- /src/components/order/address.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | class Address extends Component { 5 | render() { 6 | let {address} = this.props; 7 | return ( 8 |
    9 |

    10 | { address.full_name } 11 |

    12 | 13 |

    14 | { address.address1 } 15 |

    16 | 17 |

    18 | { address.address2 } 19 |

    20 | 21 |

    22 | { `${ address.state.name }, ${ address.city } - ${ address.zipcode }` } 23 |

    24 | 25 |

    26 | { address.country.name } 27 |

    28 | 29 |

    30 | 36 | { address.phone } 37 |

    38 |
    39 | ); 40 | }; 41 | }; 42 | 43 | export default Address; 44 | -------------------------------------------------------------------------------- /src/containers/user-signup-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import UserSignUp from '../components/user-signup'; 4 | import Actions from '../actions'; 5 | import UserAPI from '../apis/user'; 6 | 7 | function mapDispatchToProps(dispatch) { 8 | return { 9 | submitSignupForm: (formData) => { 10 | dispatch(Actions.displayLoader()); 11 | 12 | let signupPromise = UserAPI.signup(formData); 13 | 14 | signupPromise.then((response) => { 15 | dispatch(Actions.hideLoader()); 16 | dispatch(Actions.login(response.body)); 17 | dispatch(Actions.showFlash('Successfully registered')); 18 | }, 19 | (error) => { 20 | dispatch(Actions.showFlash('There was a problem in registration', 'danger')); 21 | dispatch(Actions.logout()); 22 | dispatch(Actions.hideLoader()); 23 | }); 24 | 25 | return signupPromise; 26 | } 27 | }; 28 | } 29 | 30 | const UserSignupConnector = connect(null, mapDispatchToProps)(UserSignUp); 31 | 32 | export default UserSignupConnector; 33 | -------------------------------------------------------------------------------- /src/apis/user.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | 3 | const UserAPI = { 4 | login: (params) => { 5 | return request 6 | .post(`${ process.env.REACT_APP_AMS_API_BASE }/users/token`) 7 | .set('Accept', 'application/json') 8 | .send(params); 9 | }, 10 | signup: (params) => { 11 | // If GUEST user signup is enabled in spree without API Key, 12 | // then no need to send the x-spree-token for this action, 13 | // By Default REACT_APP_ALLOW_GUEST_SIGNUP is false 14 | let allowGuestSignup; 15 | 16 | try { 17 | allowGuestSignup = JSON.parse(process.env.REACT_APP_ALLOW_GUEST_SIGNUP); 18 | } 19 | catch (e) { 20 | allowGuestSignup = true 21 | } 22 | 23 | const spreeAPIToken = allowGuestSignup ? '' : process.env.REACT_APP_SPREE_API_TOKEN; 24 | return request 25 | .post(`${ process.env.REACT_APP_API_BASE }/users`) 26 | .set('X-Spree-Token', spreeAPIToken ) 27 | .set('Content-Type', 'application/json') 28 | .send(params); 29 | } 30 | }; 31 | 32 | 33 | export default UserAPI; 34 | -------------------------------------------------------------------------------- /src/components/product/properties.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class ProductProperties extends Component { 4 | render() { 5 | let renderString = null; 6 | 7 | let productProperties = this.props.properties.map((property, idx) => { 8 | return ( 9 |
    10 |
    11 | {property.property_name} 12 |
    13 | 14 |
    15 | { property.value } 16 |
    17 |
    18 | ); 19 | }); 20 | 21 | if (productProperties.length > 0) { 22 | renderString =
    23 | { productProperties } 24 |
    ; 25 | } 26 | 27 | return ( 28 |
    29 | { renderString } 30 |
    31 | ); 32 | }; 33 | }; 34 | 35 | export default ProductProperties; 36 | -------------------------------------------------------------------------------- /src/components/order/line-item.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import URLSanitizer from '../../services/url-sanitizer'; 4 | 5 | class LineItem extends Component { 6 | render() { 7 | let { lineItem } = this.props; 8 | let image = lineItem.variant.images[0]; 9 | let imageUrl = URLSanitizer.makeAbsolute(image.mini_url); 10 | 11 | return ( 12 |
    13 |
    14 | { 17 |
    18 | 19 |
    20 | { lineItem.variant.name } 21 |

    22 | { `${ lineItem.variant.display_price } x ${ lineItem.quantity }` } 23 |

    24 |
    25 |
    26 | ); 27 | }; 28 | }; 29 | 30 | export default LineItem; 31 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import { routerMiddleware } from 'react-router-redux'; 3 | import thunk from 'redux-thunk'; 4 | import createLogger from 'redux-logger'; 5 | import reduxReset from 'redux-reset'; 6 | 7 | import history from './browser-history'; 8 | import localStorageAPI from './services/local-storage-api'; 9 | import AppReducer from './reducers/index'; 10 | 11 | /* Building a store */ 12 | const logger = createLogger(); 13 | 14 | let spreeStoreVariable; 15 | const dataFromLocalStorage = localStorageAPI.load(); 16 | 17 | if (dataFromLocalStorage) { 18 | spreeStoreVariable = createStore(AppReducer, {order: dataFromLocalStorage.order, user: dataFromLocalStorage.user, placedOrder: dataFromLocalStorage.placedOrder, locale: dataFromLocalStorage.locale}, applyMiddleware(thunk, routerMiddleware(history), logger), reduxReset()); 19 | } 20 | else { 21 | spreeStoreVariable = createStore(AppReducer, applyMiddleware(thunk, routerMiddleware(history), logger), reduxReset()); 22 | } 23 | 24 | const spreeStore = spreeStoreVariable; 25 | 26 | export default spreeStore; 27 | -------------------------------------------------------------------------------- /src/components/checkout-steps/address/country-field.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | class CountryField extends Component{ 4 | 5 | constructor(props) { 6 | super(props); 7 | this.handleCountryChange = this.handleCountryChange.bind(this); 8 | }; 9 | 10 | handleCountryChange(event) { 11 | this.props.handleCountryChange(event.currentTarget.value); 12 | // Trigger the redux-form onChange callback. 13 | this.props.input.onChange(event.currentTarget.value); 14 | }; 15 | 16 | render() { 17 | let countryOptionsMarkup = this.props.countries.map((country, idx) => { 18 | return ( 19 | 22 | ); 23 | }); 24 | 25 | return ( 26 | 32 | ); 33 | }; 34 | }; 35 | 36 | export default CountryField; 37 | -------------------------------------------------------------------------------- /src/containers/user-login-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import UserLogin from '../components/user-login'; 4 | import Actions from '../actions'; 5 | import UserAPI from '../apis/user.js'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return {}; 9 | }; 10 | 11 | const mapDispatchToProps = (dispatch) => { 12 | return { 13 | submitLoginForm: (formData) => { 14 | dispatch(Actions.displayLoader()); 15 | 16 | let loginPromise = UserAPI.login(formData) 17 | 18 | loginPromise.then((response) => { 19 | dispatch(Actions.hideLoader()); 20 | dispatch(Actions.login(response.body)); 21 | dispatch(Actions.showFlash('Successfully logged in')); 22 | }, 23 | (error) => { 24 | dispatch(Actions.showFlash('Invalid email or password.', 'danger')); 25 | dispatch(Actions.logout()); 26 | dispatch(Actions.hideLoader()); 27 | }) 28 | 29 | return loginPromise; 30 | } 31 | }; 32 | }; 33 | 34 | const UserLoginConnector = connect(mapStateToProps, mapDispatchToProps)(UserLogin); 35 | 36 | export default UserLoginConnector; 37 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Shubham Gupta 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/containers/product-tile-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Actions from '../actions'; 4 | import ProductTile from '../components/product-tile'; 5 | 6 | const mapStateToProps = (state, ownProps) => { 7 | let lineItem = state.order.line_items.find((lineItem) => { 8 | return (ownProps.product.variants_including_master_ids.indexOf(lineItem.variant_id) !== -1); 9 | }); 10 | 11 | return { 12 | productInCart: !!lineItem 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | addProductToCart: (variantId, quantity = 1) => { 19 | dispatch (Actions.addProductToCart(variantId, quantity)).then((response) => { 20 | dispatch (Actions.refreshOrder()); 21 | dispatch (Actions.showFlash('Product Successfully added to the cart!!')); 22 | }, 23 | (error) => { 24 | dispatch (Actions.showFlash('Failed to add product to cart. Please try again later!', 'danger')); 25 | }); 26 | } 27 | }; 28 | }; 29 | 30 | const ProductTileConnector = connect(mapStateToProps, mapDispatchToProps)(ProductTile); 31 | 32 | export default ProductTileConnector; 33 | -------------------------------------------------------------------------------- /src/containers/checkout-steps/address-fields-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Actions from '../../actions'; 4 | import AddressFields from '../../components/checkout-steps/address/address-fields'; 5 | import StateAPI from '../../apis/state'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | countries: state.countryList.countries 10 | }; 11 | }; 12 | 13 | const mapDispatchToProps = (dispatch) => { 14 | return { 15 | fetchStatesForCountry: (selectedCountryId) => { 16 | dispatch (Actions.displayLoader()); 17 | let stateAPIPromise = StateAPI.getByCountry(selectedCountryId).then((response) =>{ 18 | dispatch (Actions.hideLoader()); 19 | return response.body; 20 | }, 21 | (error) => { 22 | dispatch (Actions.showFlash(error.response.body.error)); 23 | dispatch (Actions.hideLoader()); 24 | return { states: [] }; 25 | }); 26 | 27 | return stateAPIPromise; 28 | } 29 | }; 30 | }; 31 | 32 | const AddressFieldsConnector = connect(mapStateToProps, mapDispatchToProps)(AddressFields); 33 | 34 | export default AddressFieldsConnector; 35 | -------------------------------------------------------------------------------- /src/apis/checkout.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | import SpreeAPIOrderAdapter from './ams-adapters/spree-api-order-adapter'; 3 | 4 | const CheckoutAPI = { 5 | next: (orderNumber, params, formData={}) => { 6 | 7 | return request 8 | .put(`${process.env.REACT_APP_AMS_API_BASE}/checkouts/${orderNumber}/next`) 9 | .query(params) 10 | .set('Accept', 'application/json') 11 | .send(formData) 12 | .then((response) => { 13 | let processedResponse = SpreeAPIOrderAdapter.processItem(response.body); 14 | response.body = processedResponse; 15 | 16 | return response; 17 | }); 18 | }, 19 | 20 | update: (orderNumber, tokenParam, formData = {}) => { 21 | return request 22 | .put(`${process.env.REACT_APP_AMS_API_BASE}/checkouts/${orderNumber}`) 23 | .query(tokenParam) 24 | .set('Accept', 'application/json') 25 | .send(formData) 26 | .then((response) => { 27 | let processedResponse = SpreeAPIOrderAdapter.processItem(response.body); 28 | response.body = processedResponse; 29 | 30 | return response; 31 | }); 32 | } 33 | } 34 | 35 | export default CheckoutAPI; 36 | -------------------------------------------------------------------------------- /src/actions/products.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | import TaxonFinder from '../services/taxon-finder'; 4 | import ProductsAPI from '../apis/products'; 5 | 6 | const products = { 7 | addProducts: (productsResponse) => { 8 | return { 9 | type: APP_ACTIONS.ADD_PRODUCTS, 10 | payload: productsResponse 11 | }; 12 | }, 13 | 14 | appendProducts: (productsResponse) => { 15 | return { 16 | type: APP_ACTIONS.APPEND_PRODUCTS, 17 | payload: productsResponse 18 | }; 19 | }, 20 | 21 | addProduct: (product) => { 22 | return { 23 | type: APP_ACTIONS.ADD_PRODUCT, 24 | payload: product 25 | } 26 | }, 27 | 28 | fetchProducts: (paramsToMerge = {}) => { 29 | return (dispatch, getState) => { 30 | let currentPathName = getState().routing.location.pathname; 31 | let taxon = TaxonFinder.findByPermalink(currentPathName, getState().taxons); 32 | 33 | if (taxon) { 34 | paramsToMerge.taxonId = taxon.id; 35 | paramsToMerge.searchTerm = ''; 36 | } 37 | 38 | return ProductsAPI.getList(paramsToMerge); 39 | } 40 | } 41 | }; 42 | 43 | export default products; 44 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 2 | // injected into the application via DefinePlugin in Webpack configuration. 3 | 4 | var REACT_APP = /^REACT_APP_/i; 5 | 6 | function getClientEnvironment(publicUrl) { 7 | var processEnv = Object 8 | .keys(process.env) 9 | .filter(key => REACT_APP.test(key)) 10 | .reduce((env, key) => { 11 | env[key] = JSON.stringify(process.env[key]); 12 | return env; 13 | }, { 14 | // Useful for determining whether we’re running in production mode. 15 | // Most importantly, it switches React into the correct mode. 16 | 'NODE_ENV': JSON.stringify( 17 | process.env.NODE_ENV || 'development' 18 | ), 19 | // Useful for resolving the correct path to static assets in `public`. 20 | // For example, . 21 | // This should only be used as an escape hatch. Normally you would put 22 | // images into the `src` and `import` them in code to get their paths. 23 | 'PUBLIC_URL': JSON.stringify(publicUrl) 24 | }); 25 | return {'process.env': processEnv}; 26 | } 27 | 28 | module.exports = getClientEnvironment; 29 | -------------------------------------------------------------------------------- /src/containers/order/list-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import OrderList from '../../components/order/list'; 6 | import OrdersAPI from '../../apis/order'; 7 | import APP_ROUTES from '../../constants/app-routes'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | orders: state.orderList.orders, 12 | user: state.user 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | loadOrders: (userAPIToken) => { 19 | dispatch (Actions.displayLoader()); 20 | 21 | return OrdersAPI.mine(userAPIToken).then((response) => { 22 | let fetchedOrders = response.body; 23 | 24 | dispatch (Actions.addOrders(fetchedOrders)); 25 | dispatch (Actions.hideLoader()); 26 | }); 27 | }, 28 | 29 | handleUserNotLoggedIn: () => { 30 | dispatch(push(APP_ROUTES.homePageRoute)); 31 | dispatch(Actions.showFlash("Please Sign in to view your orders")); 32 | } 33 | }; 34 | }; 35 | 36 | const OrderListConnector = connect(mapStateToProps, mapDispatchToProps)(OrderList); 37 | 38 | export default OrderListConnector; 39 | -------------------------------------------------------------------------------- /src/containers/header-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Header from '../components/shared/header'; 5 | import Actions from '../actions'; 6 | import TaxonAPI from '../apis/taxons'; 7 | import APP_ROUTES from '../constants/app-routes'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | taxons: state.taxons, 12 | user: state.user 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | fetchTaxons: (taxons) => { 19 | if (taxons.length === 0) { 20 | dispatch (Actions.displayLoader()); 21 | 22 | TaxonAPI.getList().then((response) => { 23 | dispatch (Actions.addTaxons(response.body.taxons)); 24 | dispatch (Actions.hideLoader()); 25 | }); 26 | } 27 | }, 28 | 29 | goToUserOrders: () => { 30 | dispatch (push(APP_ROUTES.ordersPageRoute)); 31 | }, 32 | 33 | logout: () => { 34 | dispatch(Actions.logout()); 35 | dispatch(push(APP_ROUTES.homePageRoute)); 36 | } 37 | }; 38 | }; 39 | 40 | const HeaderConnector = connect(mapStateToProps, mapDispatchToProps)(Header); 41 | 42 | export default HeaderConnector; 43 | -------------------------------------------------------------------------------- /src/components/styles/components/home-slider.scss: -------------------------------------------------------------------------------- 1 | $sliderHeight: 500px; 2 | $sliderHeightTablet: 350px; 3 | $sliderHeightMobile: 250px; 4 | 5 | .homeSlider, 6 | .homeSliderRow, 7 | .sliderContainer { 8 | height: $sliderHeight; 9 | } 10 | 11 | .homeSliderTextContent { 12 | width: 60%; 13 | padding: 0 50px; 14 | position: relative; 15 | top: 50%; 16 | color: #fff; 17 | -moz-transform: translateY(-50%); 18 | -ms-transform: translateY(-50%); 19 | -webkit-transform: translateY(-50%); 20 | transform: translateY(-50%); 21 | 22 | h3 { 23 | font-size: 24px; 24 | } 25 | 26 | p { 27 | margin-top: 20px; 28 | font-size: 16px; 29 | } 30 | } 31 | 32 | @media (max-width: 767px) { 33 | .homeSlider, 34 | .homeSliderRow, 35 | .sliderContainer { 36 | height: $sliderHeightMobile; 37 | } 38 | 39 | .homeSliderTextContent { 40 | width: 100%; 41 | padding: 0 15px; 42 | 43 | h3 { 44 | font-size: 16px; 45 | } 46 | 47 | p { 48 | margin-top: 12px; 49 | font-size: 12px; 50 | } 51 | } 52 | } 53 | 54 | @media (min-width: 768px) and (max-width: 1023px) { 55 | .homeSlider, 56 | .homeSliderRow, 57 | .sliderContainer { 58 | height: $sliderHeightTablet; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | Spree On React 17 | 18 | 19 |
    20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/containers/taxon-filters/filter-bar-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import FilterBar from '../../components/taxon-filters/filter-bar'; 6 | import APP_ROUTES from '../../constants/app-routes'; 7 | 8 | const mapStateToProps = (state, ownProps) => { 9 | return { 10 | taxons: state.taxons 11 | }; 12 | }; 13 | 14 | const mapDispatchToProps = (dispatch) => { 15 | return { 16 | handleTaxonClick: (taxon_permalink) => { 17 | dispatch (Actions.displayLoader()); 18 | dispatch (push('/t/' + taxon_permalink)); 19 | 20 | dispatch (Actions.fetchProducts()).then((response) => { 21 | 22 | if(response.statusCode === 200) { 23 | dispatch (Actions.addProducts(response.body)); 24 | dispatch (Actions.hideLoader()); 25 | } 26 | else { 27 | dispatch (Actions.showFlash('Sorry, unable to fetch products at this time. Please try again later.', 'danger')); 28 | dispatch (push(APP_ROUTES.homePageRoute)); 29 | } 30 | }); 31 | } 32 | }; 33 | }; 34 | 35 | const FilterBarConnector = connect(mapStateToProps, mapDispatchToProps)(FilterBar); 36 | 37 | export default FilterBarConnector; 38 | -------------------------------------------------------------------------------- /src/components/shared/header/styles/header-styles.scss: -------------------------------------------------------------------------------- 1 | .navBarHeader { 2 | height: 45px; 3 | padding: 10px 0 0; 4 | display: inline-block; 5 | 6 | .headerLogo { 7 | max-width: 100%; 8 | max-height: 100%; 9 | } 10 | } 11 | 12 | .headerLanguageButton { 13 | border: none !important; 14 | padding: 0 !important; 15 | color: #000 !important; 16 | font-family: 'Roboto', sans-serif !important; 17 | font-size: 13px !important; 18 | 19 | &:hover, 20 | &:focus { 21 | color: #00888a !important; 22 | text-decoration: none !important; 23 | } 24 | 25 | &+ ul { 26 | border: solid 1px #c7c7c7; 27 | border-radius: 0; 28 | top: 23px; 29 | 30 | &:before { 31 | content: ''; 32 | width: 10px; 33 | height: 10px; 34 | border: none; 35 | border-top: solid 1px #c7c7c7; 36 | border-left: solid 1px #c7c7c7; 37 | position: absolute; 38 | top: -6px; 39 | left: 10px; 40 | background: #fff; 41 | -webkit-transform: rotate(45deg); 42 | -moz-transform: rotate(45deg); 43 | transform: rotate(45deg); 44 | } 45 | } 46 | } 47 | 48 | .headerLanguage { 49 | display: inline-block; 50 | } 51 | 52 | @media (max-width: 767px) { 53 | .navBarHeader { 54 | height: 40px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/containers/search-form-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../actions'; 5 | import ProductsAPI from '../apis/products'; 6 | import SearchForm from '../components/shared/header/search-form'; 7 | import UrlParser from '../services/url-parser'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | searchTerm: UrlParser.getQueryVariable('searchTerm') 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | submitSearchForm: (searchTerm) => { 18 | dispatch (Actions.displayLoader()); 19 | 20 | ProductsAPI.getList({searchTerm: searchTerm}).then((response) => { 21 | let fetchedProducts = response.body; 22 | 23 | if (fetchedProducts.products.length === 0) { 24 | dispatch (Actions.showFlash(`No products found matching ${ searchTerm } `, 'danger')); 25 | } 26 | else { 27 | dispatch (Actions.addProducts(fetchedProducts)); 28 | dispatch (push(`/search/${ searchTerm }`)); 29 | } 30 | 31 | dispatch (Actions.hideLoader()); 32 | }); 33 | 34 | } 35 | }; 36 | }; 37 | 38 | const SearchFormConnector = connect(mapStateToProps, mapDispatchToProps)(SearchForm); 39 | 40 | export default SearchFormConnector; 41 | -------------------------------------------------------------------------------- /src/connected-intl-provider.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { IntlProvider } from 'react-intl'; 3 | 4 | import localeData from './../translations/app-translations.json'; 5 | import SUPPORTED_LOCALES from './constants/supported-locales'; 6 | 7 | function mapStateToProps(state) { 8 | /* 9 | Define user's language. Different browsers have the user locale defined 10 | on different fields on the `navigator` object, so we make sure to account 11 | for these different by checking all of them. 12 | */ 13 | let language = state.locale.currentLocale; 14 | if (!Object.keys(SUPPORTED_LOCALES).includes(language)) { 15 | language = (navigator.languages && navigator.languages[0]) || 16 | navigator.language || 17 | navigator.userLanguage; 18 | 19 | } 20 | 21 | /* 22 | Split locales with a region code 23 | */ 24 | const languageWithoutRegionCode = language.toLowerCase().split(/[_-]+/)[0]; 25 | 26 | /* 27 | Try full locale, try locale without region code, fallback to 'en' 28 | */ 29 | const messages = localeData[language] || 30 | localeData[languageWithoutRegionCode] || 31 | localeData.en; 32 | 33 | return ({ locale: state.locale.currentLocale, messages: messages }); 34 | } 35 | 36 | export default connect(mapStateToProps)(IntlProvider); 37 | -------------------------------------------------------------------------------- /src/components/taxon-filters/filter-bar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { NavDropdown } from 'react-bootstrap'; 3 | import Taxon from './taxon'; 4 | import styles from './styles/filter-bar.scss'; 5 | 6 | class FilterBar extends Component { 7 | taxonMarkup (taxon) { 8 | if (taxon.taxons.length > 0) { 9 | let thisTaxonInnerMarkup = taxon.taxons.map((innerTaxon) => { 10 | return (this.taxonMarkup(innerTaxon)); 11 | }); 12 | 13 | return ( 14 | 15 | { thisTaxonInnerMarkup } 16 | 17 | ); 18 | } 19 | else { 20 | return ( 21 | 22 | ); 23 | } 24 | }; 25 | 26 | render() { 27 | let parentTaxons = this.props.taxons.filter((taxon) => { 28 | return taxon.parent_id == null; 29 | }); 30 | 31 | const taxonFilterMarkup = parentTaxons.map((parentTaxon) => { 32 | return this.taxonMarkup(parentTaxon); 33 | }); 34 | 35 | return ( 36 |
      37 | { taxonFilterMarkup } 38 |
    39 | ); 40 | } 41 | }; 42 | 43 | export default FilterBar; 44 | -------------------------------------------------------------------------------- /src/containers/product/product-show-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import ProductShow from '../../components/product/show'; 4 | import Actions from '../../actions'; 5 | import ProductsAPI from '../../apis/products'; 6 | 7 | const mapStateToProps = (state, ownProps) => { 8 | return { 9 | products: state.productList.products, 10 | displayLoader: state.displayLoader, 11 | lineItems: state.order.line_items 12 | }; 13 | }; 14 | 15 | const mapDispatchToProps = (dispatch) => { 16 | return { 17 | fetchProductFromAPI: (productId) => { 18 | dispatch (Actions.displayLoader()); 19 | 20 | ProductsAPI.getItem(productId).then((response) => { 21 | dispatch (Actions.addProduct(response.body)); 22 | dispatch (Actions.hideLoader()); 23 | }); 24 | }, 25 | 26 | addProductToCart: (variantId, quantity = 1) => { 27 | dispatch (Actions.addProductToCart(variantId, quantity)).then((response) => { 28 | dispatch (Actions.refreshOrder()); 29 | dispatch (Actions.showFlash('Product Successfully added to the cart!!')); 30 | }, 31 | (error) => { 32 | dispatch (Actions.showFlash('Failed to add product to cart. Please try again later!', 'danger')); 33 | }); 34 | } 35 | }; 36 | }; 37 | 38 | const ProductShowConnector = connect(mapStateToProps, mapDispatchToProps)(ProductShow); 39 | 40 | export default ProductShowConnector; 41 | -------------------------------------------------------------------------------- /src/reducers/product-list.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | import ProductModel from '../services/product-model'; 3 | 4 | const initialState = { 5 | products: [], 6 | meta: {} 7 | }; 8 | 9 | const productList = function(state = initialState, action) { 10 | let newProductList; 11 | let productInList; 12 | let oldProductList; 13 | 14 | switch (action.type) { 15 | case APP_ACTIONS.ADD_PRODUCTS: 16 | return Object.assign({}, state, action.payload); 17 | 18 | case APP_ACTIONS.APPEND_PRODUCTS: 19 | newProductList = action.payload.products.map((product) => { return product.id }); 20 | oldProductList = state.products.filter ((product) => { 21 | return newProductList.indexOf(product.id) === -1; 22 | }); 23 | 24 | return ( Object.assign ( 25 | {}, 26 | action.payload, 27 | { products: [...oldProductList, ...action.payload.products] } 28 | )); 29 | 30 | case APP_ACTIONS.ADD_PRODUCT: 31 | productInList = ProductModel.find(action.payload.id, state.products); 32 | 33 | if (productInList) { 34 | return state; 35 | } 36 | else { 37 | newProductList = Object.assign( [], state.products ); 38 | newProductList.push(action.payload); 39 | return Object.assign ( {}, state, { products: newProductList } ); 40 | } 41 | 42 | default: 43 | return state; 44 | } 45 | } 46 | 47 | export default productList; 48 | -------------------------------------------------------------------------------- /src/components/shared/header/search-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import styles from './styles/search-form.scss'; 3 | 4 | class SearchForm extends Component { 5 | constructor (props) { 6 | super(props); 7 | this.submitSearchForm = this.submitSearchForm.bind(this); 8 | this.onSearchInputChange = this.onSearchInputChange.bind(this) 9 | this.state = { searchTerm: '' }; 10 | }; 11 | 12 | onSearchInputChange (event) { 13 | this.setState ({ searchTerm: event.target.value }); 14 | }; 15 | 16 | submitSearchForm (event) { 17 | event.preventDefault(); 18 | this.props.submitSearchForm(this.state.searchTerm); 19 | }; 20 | 21 | render () { 22 | // TODO 23 | return ( 24 |
    25 | 26 | 27 |
    28 | 33 |
    34 |
    35 | ); 36 | }; 37 | 38 | }; 39 | 40 | export default SearchForm; 41 | -------------------------------------------------------------------------------- /src/components/home-page.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import ProductList from './product-list'; 5 | import HomeSlider from './home-slider'; 6 | import Loader from './shared/loader'; 7 | import Layout from './layout'; 8 | 9 | class HomePage extends Component { 10 | 11 | componentDidMount() { 12 | this.props.triggerInitialSetup(this.props.match.params['searchTerm']); 13 | }; 14 | 15 | /* If home page icon is clicked. */ 16 | componentDidUpdate(prevProps, prevState) { 17 | if (prevProps.location.pathname !== this.props.location.pathname) { 18 | this.props.triggerInitialSetup(this.props.location.pathname); 19 | } 20 | } 21 | 22 | render() { 23 | return ( 24 | 25 |
    26 | 27 | 28 | 29 |
    30 |

    31 | 35 |

    36 | 37 | 40 |
    41 |
    42 |
    43 | ); 44 | }; 45 | }; 46 | 47 | export default HomePage; 48 | -------------------------------------------------------------------------------- /src/components/styles/pages/orders.scss: -------------------------------------------------------------------------------- 1 | .order-section { 2 | padding: 50px 0; 3 | 4 | .order-list { 5 | margin-top: 40px; 6 | } 7 | 8 | .order-block-header { 9 | border-bottom: solid 1px $border-grey; 10 | padding-bottom: 10px; 11 | } 12 | 13 | .order-header-labels { 14 | margin-top: 5px; 15 | color: $color-black; 16 | 17 | .label { 18 | margin-right: 10px; 19 | padding: 0; 20 | color: $color-black; 21 | 22 | &.label-default { 23 | padding: 5px 10px; 24 | color: $color-white; 25 | } 26 | } 27 | } 28 | 29 | .order-line-items { 30 | margin-top: 30px; 31 | } 32 | 33 | .order-items-row { 34 | margin: 0; 35 | border-bottom: solid 1px $border-grey; 36 | padding: 15px 0; 37 | 38 | &:first-child { 39 | padding-top: 0; 40 | } 41 | } 42 | 43 | .order-items-image-block { 44 | padding-left: 0; 45 | text-align: center; 46 | } 47 | 48 | .order-address-block { 49 | border: solid 1px $border-grey; 50 | padding: 15px; 51 | background: $light-grey-bg; 52 | } 53 | 54 | .order-total-row { 55 | padding-top: 15px; 56 | 57 | font-size: 16px; 58 | font-weight: 500; 59 | 60 | small { 61 | margin-right: 10px; 62 | font-size: 13px; 63 | font-weight: 300; 64 | } 65 | } 66 | 67 | .confirmation-details-block { 68 | margin-top: 30px; 69 | border: solid 1px $border-grey; 70 | padding: 15px; 71 | 72 | &:first-child { 73 | margin-top: 0; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/apis/products.js: -------------------------------------------------------------------------------- 1 | import APP_DEFAULTS from '../constants/app-defaults'; 2 | import SpreeAPIProductAdapter from './ams-adapters/spree-api-product-adapter'; 3 | 4 | var request = require('superagent'); 5 | 6 | const ProductsAPI = { 7 | 8 | getList: (params = {}) => { 9 | let apiBase = process.env.REACT_APP_AMS_API_BASE; 10 | let sanitizedQueryParams = {}; 11 | 12 | sanitizedQueryParams.page = params.page_no || 1; 13 | sanitizedQueryParams.per_page = APP_DEFAULTS.perPage; 14 | sanitizedQueryParams['q[name_cont]'] = params.searchTerm || ''; 15 | 16 | if (params['taxonId']){ 17 | sanitizedQueryParams.taxon_id = params.taxonId; 18 | } 19 | 20 | return request 21 | .get(`${ apiBase }/products`) 22 | .query(sanitizedQueryParams) 23 | .set('Accept', 'application/json') 24 | .then( 25 | (response) => { 26 | let processedResponse = SpreeAPIProductAdapter.processList(response.body); 27 | response.body = processedResponse; 28 | 29 | return response; 30 | } 31 | ); 32 | }, 33 | 34 | getItem: (productId) => { 35 | return request 36 | .get(`${process.env.REACT_APP_AMS_API_BASE}/products/` + productId) 37 | .set('Accept', 'application/json') 38 | .then( 39 | (response) => { 40 | let processedResponse = SpreeAPIProductAdapter.processItem(response.body); 41 | response.body = processedResponse; 42 | 43 | return response; 44 | } 45 | ); 46 | } 47 | }; 48 | 49 | export default ProductsAPI; 50 | -------------------------------------------------------------------------------- /src/actions/index.js: -------------------------------------------------------------------------------- 1 | import products from './products'; 2 | import taxons from './taxons'; 3 | import loader from './loader'; 4 | import order from './order'; 5 | import orderList from './order-list'; 6 | import flash from './flash'; 7 | import countries from './countries'; 8 | import checkout from './checkout'; 9 | import placedOrder from './placed-order'; 10 | import user from './user'; 11 | import locale from './locale'; 12 | 13 | export default { 14 | addProducts: products.addProducts, 15 | appendProducts: products.appendProducts, 16 | addProduct: products.addProduct, 17 | addOrders: orderList.addOrders, 18 | addOrder: orderList.addOrder, 19 | fetchProducts: products.fetchProducts, 20 | addTaxons: taxons.addTaxons, 21 | displayLoader: loader.displayLoader, 22 | hideLoader: loader.hideLoader, 23 | addProductToCart: order.addProductToCart, 24 | emptyCart: order.emptyCart, 25 | clearOrder: order.clearOrder, 26 | setFlash: flash.setFlash, 27 | showFlash: flash.showFlash, 28 | hideFlash: flash.hideFlash, 29 | removeProductFromCart: order.removeProductFromCart, 30 | changeProductQuantityFromCart: order.changeProductQuantityFromCart, 31 | updateOrderInState: order.updateOrderInState, 32 | refreshOrder: order.refreshOrder, 33 | addLineItem: order.addLineItem, 34 | addCountries: countries.addCountries, 35 | goToNextStep: checkout.goToNextStep, 36 | addPlacedOrder: placedOrder.addPlacedOrder, 37 | clearPlacedOrder: placedOrder.clearPlacedOrder, 38 | login: user.login, 39 | logout: user.logout, 40 | setLocale: locale.setLocale 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/order/list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import Layout from '../layout'; 5 | import OrderPanelView from './panel-view'; 6 | import Loader from '../shared/loader'; 7 | 8 | class OrderList extends Component { 9 | constructor(props){ 10 | super(props); 11 | 12 | this.state = { 13 | displayLoader: true 14 | }; 15 | }; 16 | 17 | componentDidMount() { 18 | if (this.props.user.id) { 19 | this.props.loadOrders(this.props.user.token).then((response) => { 20 | this.setState({ displayLoader: false }); 21 | }); 22 | } 23 | else { 24 | this.props.handleUserNotLoggedIn(); 25 | } 26 | }; 27 | 28 | render() { 29 | let orderListMarkup = this.props.orders.map((order, idx) => { 30 | return (); 31 | }); 32 | 33 | return ( 34 | 35 | 36 |
    37 |
    38 |
    39 | 43 |
    44 |
    45 | { orderListMarkup } 46 |
    47 |
    48 |
    49 |
    50 | ); 51 | }; 52 | }; 53 | 54 | export default OrderList; 55 | -------------------------------------------------------------------------------- /src/apis/ams-adapters/spree-api-product-adapter.js: -------------------------------------------------------------------------------- 1 | import ProductM from '../../services/product-model'; 2 | 3 | const SpreeAPIProductAdapter = { 4 | processList: (productsListAMS) => { 5 | productsListAMS.products.forEach((product) => { 6 | SpreeAPIProductAdapter._process(product, productsListAMS); 7 | }); 8 | 9 | return productsListAMS; 10 | }, 11 | 12 | processItem: (productAMS) => { 13 | let product = productAMS.product; 14 | SpreeAPIProductAdapter._process(product, productAMS); 15 | 16 | return product; 17 | }, 18 | 19 | /* 20 | PRIVATE METHODS 21 | */ 22 | _addMasterImages: (product, productsListAMS) => { 23 | product.master.images = ProductM.images(product.master.image_ids, productsListAMS); 24 | }, 25 | 26 | _addVariantImages: (product, productsListAMS) => { 27 | product.variants.forEach((variant) => { 28 | variant.images = ProductM.images(variant.image_ids, productsListAMS); 29 | }); 30 | }, 31 | 32 | _process: (product, AMSResponse) => { 33 | product.images = ProductM.images(product.image_ids, AMSResponse); 34 | product.variants = ProductM.variantsExcludingMaster(product.variants_including_master_ids, AMSResponse); 35 | product.master = ProductM.masterVariant(product.variants_including_master_ids, AMSResponse); 36 | product.product_properties = ProductM.properties(product.product_property_ids, AMSResponse); 37 | 38 | SpreeAPIProductAdapter._addMasterImages(product, AMSResponse); 39 | SpreeAPIProductAdapter._addVariantImages(product, AMSResponse); 40 | } 41 | 42 | }; 43 | 44 | export default SpreeAPIProductAdapter; 45 | -------------------------------------------------------------------------------- /src/containers/checkout-steps/checkout-success-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import APP_ACTIONS from '../../constants/app-actions'; 6 | import CheckoutSuccessPage from '../../components/checkout-steps/checkout-success-page'; 7 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 8 | import APP_ROUTES from '../../constants/app-routes'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | return { 12 | order: state.order, 13 | placedOrder: state.placedOrder 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | saveOrderAsPlaced: (order) => { 20 | dispatch(Actions.addPlacedOrder(order)); 21 | }, 22 | 23 | setCurrentCheckoutStep: () => { 24 | dispatch({ 25 | type: APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP, 26 | payload: 'complete' 27 | }); 28 | }, 29 | 30 | handleCheckoutStepNotEditable: (order) => { 31 | if (order.id) { 32 | const previousStep = CheckoutStepCalculator.previous(order.checkout_steps, 'complete'); 33 | 34 | dispatch ( push(APP_ROUTES.checkout[`${ previousStep }PageRoute`])); 35 | } 36 | else { 37 | dispatch ( push(APP_ROUTES.cartPageRoute)); 38 | } 39 | }, 40 | 41 | clearOrder: () => { 42 | dispatch (Actions.clearOrder()); 43 | } 44 | }; 45 | }; 46 | 47 | const CheckoutSuccessConnector = connect(mapStateToProps, mapDispatchToProps)(CheckoutSuccessPage); 48 | 49 | export default CheckoutSuccessConnector; 50 | -------------------------------------------------------------------------------- /src/components/home-slider.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import {Carousel} from 'react-bootstrap' 3 | 4 | import Banner1 from '../images/b1.jpg'; 5 | import Banner2 from '../images/b2.jpg'; 6 | 7 | import styles from './styles/components/home-slider.scss'; 8 | 9 | 10 | class HomeSlider extends Component { 11 | 12 | render () { 13 | return ( 14 | 15 | 16 |
    17 |
    18 |
    19 |

    Handpicked

    20 |

    21 | The best of global brands, at your doorstep! 22 |

    23 |
    24 |
    25 |
    26 |
    27 | 28 |
    29 |
    30 |
    31 |

    Shopaholic

    32 |

    33 | The end of reason sale! 34 |

    35 |
    36 |
    37 |
    38 |
    39 |
    40 | ) 41 | } 42 | } 43 | 44 | export default HomeSlider; 45 | -------------------------------------------------------------------------------- /src/components/product-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ProductTileConnector from '../containers/product-tile-connector'; 3 | import InfiniteScroller from './shared/infinite-scroller'; 4 | import APP_DEFAULTS from '../constants/app-defaults'; 5 | 6 | class ProductList extends Component { 7 | constructor(props){ 8 | super(props); 9 | this.currentPage = 1; 10 | }; 11 | 12 | loadMoreProducts(){ 13 | return this.props.loadMoreProducts(this.currentPage + 1); 14 | }; 15 | 16 | componentWillReceiveProps(nextProps) { 17 | this.currentPage = Math.ceil(nextProps.products.length / APP_DEFAULTS.perPage); 18 | }; 19 | 20 | render() { 21 | let infiniteScroller = null; 22 | let productList = this.props.products.map((product, idx) => { 23 | return ( ); 24 | }); 25 | 26 | if (this.props.products.length > 0) { 27 | infiniteScroller = 30 | { productList } 31 | ; 32 | } 33 | 34 | return ( 35 |
    36 |
    37 |
    38 | { infiniteScroller } 39 |
    40 |
    41 |
    42 | ); 43 | } 44 | } 45 | 46 | export default ProductList; 47 | -------------------------------------------------------------------------------- /src/components/shared/infinite-scroller.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import InfiniteScroll from 'redux-infinite-scroll'; 5 | 6 | class InfiniteScroller extends Component { 7 | constructor(props){ 8 | super(props); 9 | this.state = { 10 | loadingMore: false 11 | } 12 | }; 13 | 14 | loadMore () { 15 | this.setState({ 16 | loadingMore: true 17 | }); 18 | 19 | this.props.loadMore().then(() => { 20 | this.setState({ loadingMore: false }); 21 | }); 22 | }; 23 | 24 | render () { 25 | return ( 29 | 30 | 34 | 35 | } 36 | hasMore={ this.props.pageCount > this.props.currentPage }> 37 | 38 | { this.props.children } 39 | 40 | ); 41 | }; 42 | }; 43 | 44 | export default InfiniteScroller; 45 | -------------------------------------------------------------------------------- /src/components/product/variants-list.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { injectIntl } from 'react-intl'; 3 | import { Dropdown, Button, MenuItem } from 'react-bootstrap'; 4 | 5 | class VariantsList extends Component { 6 | 7 | render() { 8 | let variantsList, variantMenuItems; 9 | const outOfStockRep = this.props.intl.formatMessage( 10 | { id: 'label.outOfStock', 11 | defaultMessage: "Out of stock" 12 | }); 13 | 14 | variantMenuItems = this.props.variantsList.map((variant, idx) => { 15 | return ( 16 | this.props.onChangeVariant(variant) }> 17 | { variant.options_text } 18 | { variant.in_stock ? '' : ` (${ outOfStockRep })` } 19 | 20 | ); 21 | }); 22 | variantsList = ( 23 | 24 | 27 | 28 | 29 | 30 | { variantMenuItems } 31 | 32 | 33 | ); 34 | 35 | let renderString = null; 36 | if(this.props.variantsList.length > 0){ 37 | renderString =
    38 | { variantsList } 39 |
    40 | } 41 | 42 | return ( 43 | renderString 44 | ); 45 | } 46 | }; 47 | 48 | export default injectIntl(VariantsList); 49 | -------------------------------------------------------------------------------- /src/apis/line-item.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | import SpreeAPILineItemAdapter from './ams-adapters/spree-api-line-item-adapter'; 3 | 4 | const LineItemAPI = { 5 | create: (params) => { 6 | return request 7 | .post(`${process.env.REACT_APP_AMS_API_BASE}/line_items`) 8 | .query(params.tokenParam) 9 | .set('Accept', 'application/json') 10 | .send({ 11 | line_item: { 12 | variant_id: params.variantId, 13 | quantity: params.quantity 14 | }, 15 | order_number: params.orderNumber 16 | }) 17 | .then((response) => { 18 | let processedResponse = SpreeAPILineItemAdapter.processItem(response.body); 19 | response.body = processedResponse; 20 | 21 | return response; 22 | }); 23 | }, 24 | 25 | destroy: (params) => { 26 | let queryParams = Object.assign({}, { order_number: params.orderNumber }, params.tokenParam); 27 | 28 | return request 29 | .delete(`${process.env.REACT_APP_AMS_API_BASE}/line_items/${params.lineItemId}`) 30 | .query(queryParams) 31 | .set('Accept', 'application/json'); 32 | }, 33 | 34 | update: (params) => { 35 | return request 36 | .put(`${process.env.REACT_APP_AMS_API_BASE}/line_items/${params.lineItemId}`) 37 | .set('Accept', 'application/json') 38 | .query(params.tokenParam) 39 | .send({ 40 | line_item: { 41 | quantity: params.quantity 42 | }, 43 | order_number: params.orderNumber 44 | }) 45 | .then((response) => { 46 | let processedResponse = SpreeAPILineItemAdapter.processItem(response.body); 47 | response.body = processedResponse; 48 | 49 | return response; 50 | }); 51 | } 52 | }; 53 | 54 | export default LineItemAPI; 55 | -------------------------------------------------------------------------------- /src/reducers/order.js: -------------------------------------------------------------------------------- 1 | import APP_ACTIONS from '../constants/app-actions'; 2 | 3 | const initialState = { 4 | shipments: [], 5 | line_items: [], 6 | checkout_steps: [] 7 | }; 8 | 9 | const order = function(state = initialState, action) { 10 | let newLineItems = Object.assign([], state.line_items); 11 | let lineItemIndexToBeRemoved; 12 | 13 | switch (action.type) { 14 | case APP_ACTIONS.CREATE_ORDER: 15 | return action.payload; 16 | 17 | case APP_ACTIONS.ADD_PRODUCT_TO_CART: 18 | lineItemIndexToBeRemoved = state.line_items.findIndex((lineItem, idx) => { 19 | return (lineItem.variant_id === action.payload.variant_id); 20 | }); 21 | 22 | if (lineItemIndexToBeRemoved > -1) 23 | newLineItems.splice(lineItemIndexToBeRemoved, 1); 24 | 25 | newLineItems.push(action.payload); 26 | 27 | return Object.assign({}, state, { line_items: newLineItems }); 28 | // When an item is removed from cart or qty falls to zero 29 | case APP_ACTIONS.REMOVE_LINE_ITEM: 30 | newLineItems = newLineItems.filter( (lineItem) => { 31 | return lineItem.id !== action.payload; 32 | }); 33 | 34 | return Object.assign ( {}, state, { line_items: newLineItems }); 35 | // When qty is updated 36 | case APP_ACTIONS.UPDATE_LINE_ITEM: 37 | let index = state.line_items.map((item)=> item.id).indexOf(action.payload.id) 38 | var updatedLineItems = Object.assign ([], state.line_items) 39 | updatedLineItems.splice(index, 1, action.payload); 40 | 41 | return Object.assign ({}, state, { line_items: updatedLineItems }); 42 | // When `emptyCart` is called 43 | case APP_ACTIONS.DESTROY_ORDER: 44 | return initialState; 45 | default: 46 | return state; 47 | } 48 | } 49 | 50 | export default order; 51 | -------------------------------------------------------------------------------- /src/containers/checkout-steps/delivery-form-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import APP_ACTIONS from '../../constants/app-actions'; 6 | import DeliveryForm from '../../components/checkout-steps/delivery-form'; 7 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 8 | import APP_ROUTES from '../../constants/app-routes'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | return { 12 | order: state.order, 13 | placedOrder: state.placedOrder 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | setCurrentCheckoutStep: () => { 20 | dispatch ({ 21 | type: APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP, 22 | payload: 'delivery' 23 | }); 24 | }, 25 | 26 | handleDeliveryFormSubmit: (formData, order) => { 27 | dispatch (Actions.goToNextStep(order, formData)); 28 | }, 29 | 30 | handleCheckoutStepNotEditable: (order, placedOrder) => { 31 | /* Redirect to last step if order is already placed */ 32 | if (placedOrder.id) { 33 | dispatch (push(APP_ROUTES.checkout[`${ placedOrder.checkout_steps.slice(-1) }PageRoute`])); 34 | } 35 | else { 36 | if (order.id) { 37 | const previousStep = CheckoutStepCalculator.previous(order.checkout_steps, 'delivery'); 38 | 39 | dispatch ( push(APP_ROUTES.checkout[`${ previousStep }PageRoute`])); 40 | } 41 | else { 42 | dispatch ( push(APP_ROUTES.cartPageRoute)); 43 | } 44 | } 45 | } 46 | }; 47 | }; 48 | 49 | const DeliveryFormConnector = connect(mapStateToProps, mapDispatchToProps)(DeliveryForm); 50 | 51 | export default DeliveryFormConnector; 52 | -------------------------------------------------------------------------------- /src/components/shared/styles/header.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | height: 120px; 3 | border-bottom: solid 1px #cacaca; 4 | 5 | .headerLanguageBlock, 6 | .headerTopNav { 7 | padding-top: 20px; 8 | } 9 | 10 | .globalHeaderLogo { 11 | font-family: 'Oswald', sans-serif; 12 | font-size: 32px; 13 | font-weight: 700; 14 | } 15 | 16 | .headerNavHolder { 17 | margin-bottom: 0; 18 | } 19 | 20 | .headerUserLink { 21 | font-family: 'Roboto', sans-serif; 22 | font-size: 13px; 23 | } 24 | 25 | .headerUserBlock { 26 | margin-left: 20px; 27 | display: inline-block; 28 | } 29 | 30 | .headerUserButton { 31 | border: none; 32 | padding: 0; 33 | color: #000; 34 | 35 | &:hover, 36 | &:focus { 37 | color: #4eaf79; 38 | text-decoration: none; 39 | } 40 | 41 | &+ ul { 42 | border: solid 1px #c7c7c7; 43 | border-radius: 0; 44 | top: 23px; 45 | 46 | &:before { 47 | content: ''; 48 | width: 10px; 49 | height: 10px; 50 | border: none; 51 | border-top: solid 1px #c7c7c7; 52 | border-left: solid 1px #c7c7c7; 53 | position: absolute; 54 | top: -6px; 55 | left: 10px; 56 | background: #fff; 57 | -webkit-transform: rotate(45deg); 58 | -moz-transform: rotate(45deg); 59 | transform: rotate(45deg); 60 | } 61 | } 62 | } 63 | 64 | .headerBottomNavRow { 65 | margin-top: 10px; 66 | } 67 | 68 | .headerMainNavHolder { 69 | padding-top: 10px; 70 | } 71 | 72 | @media (max-width: 767px) { 73 | height: auto; 74 | 75 | .globalHeaderLogo { 76 | text-align: left; 77 | } 78 | 79 | .headerMobileMenu { 80 | margin: 15px 0 0; 81 | font-size: 18px; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/containers/checkout-steps/confirmation-form-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import APP_ACTIONS from '../../constants/app-actions'; 6 | import ConfirmationForm from '../../components/checkout-steps/confirmation-form'; 7 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 8 | import APP_ROUTES from '../../constants/app-routes'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | return { 12 | order: state.order, 13 | placedOrder: state.placedOrder 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | handleCheckoutStepNotEditable: (order, placedOrder) => { 20 | /* Redirect to last step if order is already placed */ 21 | if (placedOrder.id) { 22 | dispatch (push(APP_ROUTES.checkout[`${ placedOrder.checkout_steps.slice(-1) }PageRoute`])); 23 | } 24 | else { 25 | if (order.id) { 26 | const previousStep = CheckoutStepCalculator.previous(order.checkout_steps, 'confirm'); 27 | 28 | dispatch ( push(APP_ROUTES.checkout[`${ previousStep }PageRoute`])); 29 | } 30 | else { 31 | dispatch ( push(APP_ROUTES.cartPageRoute)); 32 | } 33 | } 34 | }, 35 | 36 | setCurrentCheckoutStep: () => { 37 | dispatch({ 38 | type: APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP, 39 | payload: 'confirm' 40 | }); 41 | }, 42 | 43 | handleFormSubmit: (formData, order) => { 44 | dispatch (Actions.goToNextStep(order, formData)); 45 | }, 46 | }; 47 | }; 48 | 49 | const CheckoutConfirmationConnector = connect(mapStateToProps, mapDispatchToProps)(ConfirmationForm); 50 | 51 | export default CheckoutConfirmationConnector; 52 | -------------------------------------------------------------------------------- /src/components/styles/components/product-tile.scss: -------------------------------------------------------------------------------- 1 | .productBlock { 2 | height: 250px; 3 | margin-bottom: 40px; 4 | 5 | &:hover { 6 | .productHoverInfo { 7 | height: 40px; 8 | opacity: 1; 9 | } 10 | } 11 | 12 | .productImageBlock { 13 | width: 100%; 14 | height: 180px; 15 | padding: 5px; 16 | display: table; 17 | position: relative; 18 | background: #f7f7f9; 19 | } 20 | 21 | .productImageHolder { 22 | width: 100%; 23 | height: 180px; 24 | display: table-cell; 25 | text-align: center; 26 | vertical-align: middle; 27 | } 28 | 29 | .productMainImage { 30 | max-width: 100%; 31 | max-height: 100%; 32 | } 33 | 34 | .productHoverInfo { 35 | width: 100%; 36 | height: 0; 37 | padding-top: 10px; 38 | position: absolute; 39 | bottom: 0; 40 | left: 0; 41 | text-align: center; 42 | background: rgba(0, 0, 0, .8); 43 | opacity: 0; 44 | -moz-transition: all .4s ease-in-out; 45 | -ms-transition: all .4s ease-in-out; 46 | -webkit-transition: all .4s ease-in-out; 47 | transition: all .4s ease-in-out; 48 | } 49 | 50 | .productHoverButton { 51 | margin: 0 10px; 52 | color: #fff; 53 | font-size: 18px; 54 | 55 | &:hover { 56 | color: #4eaf79; 57 | } 58 | } 59 | 60 | .productInfoBlock { 61 | margin-top: 10px; 62 | } 63 | 64 | .productTitle { 65 | font-family: 'Oswald', sans-serif; 66 | font-size: 16px; 67 | font-weight: 400; 68 | } 69 | 70 | @media (max-width: 767px) { 71 | height: 200px; 72 | margin-bottom: 30px; 73 | 74 | .productImageBlock, 75 | .productImageHolder { 76 | height: 140px; 77 | } 78 | 79 | .productInfoBlock { 80 | font-size: 11px; 81 | } 82 | 83 | .productTitle { 84 | font-size: 13px; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/services/product-model.js: -------------------------------------------------------------------------------- 1 | const ProductModel = { 2 | find: (productId, products = []) => { 3 | const radix = 10; 4 | let product; 5 | 6 | product = products.find((product) => { 7 | return (parseInt(product.id, radix) === parseInt(productId, radix)); 8 | }); 9 | 10 | return product; 11 | }, 12 | 13 | findBySlug: (productSlug, products = []) => { 14 | let product; 15 | 16 | product = products.find((product) => { 17 | return product.slug === productSlug; 18 | }); 19 | 20 | return product; 21 | }, 22 | 23 | variants: (variantIds, productsList = {}) => { 24 | return productsList.variants.filter((variant) => { 25 | return variantIds.indexOf(variant.id) !== -1; 26 | }); 27 | }, 28 | 29 | variantsExcludingMaster: (variantIds, productsList = {}) => { 30 | return productsList.variants.filter((variant) => { 31 | return variantIds.indexOf(variant.id) !== -1 && !variant.is_master; 32 | }); 33 | }, 34 | 35 | masterVariant: (variantIds, productsList = {}) => { 36 | return productsList.variants.find((variant) => { 37 | return variantIds.indexOf(variant.id) !== -1 && variant.is_master; 38 | }); 39 | }, 40 | 41 | images: (imageIds, productsList = {}) => { 42 | return productsList.images.filter((image) => { 43 | return imageIds.indexOf(image.id) !== -1; 44 | }); 45 | }, 46 | 47 | mainImage: (imageIds, productsList = {}) => { 48 | return productsList.images.filter((image) => { 49 | return imageIds.indexOf(image.id) !== -1 && image.position === 1; 50 | }); 51 | }, 52 | 53 | properties: (propertyIds, productsList = {}) => { 54 | return productsList.product_properties.filter((property) => { 55 | return propertyIds.indexOf(property.id) !== -1; 56 | }); 57 | } 58 | 59 | }; 60 | 61 | export default ProductModel; 62 | -------------------------------------------------------------------------------- /src/components/checkout-steps/payment/card-fields.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Field } from 'redux-form'; 3 | import { injectIntl } from 'react-intl'; 4 | 5 | import FormField from '../shared/form-field'; 6 | 7 | class CardFields extends Component{ 8 | 9 | render() { 10 | return ( 11 |
    12 | 17 | 18 | 23 | 24 | 29 | 30 | 35 |
    36 | ); 37 | }; 38 | }; 39 | 40 | export default injectIntl(CardFields); 41 | -------------------------------------------------------------------------------- /src/components/checkout-steps/checkout-success-page.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import Layout from "../layout"; 5 | import BaseCheckoutLayout from "./base-checkout-layout"; 6 | import OrderPanelView from '../order/panel-view'; 7 | 8 | class CheckoutSuccessPage extends Component { 9 | 10 | /* Render this step only if order is present and in a valid checkout state. */ 11 | componentWillMount() { 12 | if (this.props.order.state === 'complete') { 13 | let dupeOrder = Object.assign(this.props.order); 14 | this.props.saveOrderAsPlaced(dupeOrder); 15 | } 16 | else { 17 | if (!this.props.placedOrder.id) { 18 | this.props.handleCheckoutStepNotEditable(this.props.order); 19 | } 20 | } 21 | }; 22 | 23 | componentDidMount () { 24 | if (this.props.order.state === 'complete') { 25 | this.props.clearOrder(); 26 | } 27 | }; 28 | 29 | render() { 30 | return ( 31 | 32 | 36 |
    37 | 38 | 42 | 43 | 44 |
    45 |
    46 |
    47 | ); 48 | }; 49 | }; 50 | 51 | export default CheckoutSuccessPage; 52 | -------------------------------------------------------------------------------- /src/containers/home-page-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | 3 | import Actions from '../actions'; 4 | import TaxonAPI from '../apis/taxons'; 5 | import HomePage from '../components/home-page'; 6 | 7 | import UrlParser from '../services/url-parser'; 8 | 9 | const mapStateToProps = (state, ownProps) => { 10 | return { 11 | products: state.productList.products, 12 | displayLoader: state.displayLoader, 13 | pageCount: state.productList.meta.total_pages 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | triggerInitialSetup: (searchTerm = '') => { 20 | dispatch (Actions.displayLoader()); 21 | 22 | TaxonAPI.getList().then((response) => { 23 | let fetchedTaxons = response.body.taxons; 24 | dispatch (Actions.addTaxons(fetchedTaxons)); 25 | 26 | let searchTerm = UrlParser.getQueryVariable('searchTerm') || ''; 27 | 28 | dispatch (Actions.fetchProducts({ searchTerm: searchTerm })).then((response) => { 29 | let fetchedProducts = response.body; 30 | 31 | dispatch (Actions.addProducts(fetchedProducts)); 32 | dispatch (Actions.hideLoader()); 33 | }); 34 | 35 | }, 36 | 37 | (error) => { 38 | dispatch (Actions.hideLoader()); 39 | dispatch (Actions.showFlash('Unable to connect to server. Please try again later.', 'danger')); 40 | }); 41 | }, 42 | 43 | loadMoreProducts: (pageNo) => { 44 | let searchTerm = UrlParser.getQueryVariable('searchTerm') || ''; 45 | 46 | return dispatch (Actions.fetchProducts({ page_no: pageNo, searchTerm: searchTerm })).then((response) => { 47 | dispatch (Actions.appendProducts(response.body)); 48 | }); 49 | } 50 | }; 51 | }; 52 | 53 | const HomePageConnector = connect(mapStateToProps, mapDispatchToProps)(HomePage); 54 | 55 | export default HomePageConnector; 56 | -------------------------------------------------------------------------------- /src/containers/checkout-steps/payment-form-connector.jsx: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import APP_ACTIONS from '../../constants/app-actions'; 6 | import PaymentForm from '../../components/checkout-steps/payment-form'; 7 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 8 | import APP_ROUTES from '../../constants/app-routes'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | return { 12 | order: state.order, 13 | placedOrder: state.placedOrder 14 | }; 15 | }; 16 | 17 | const mapDispatchToProps = (dispatch) => { 18 | return { 19 | setCurrentCheckoutStep: () => { 20 | dispatch ({ 21 | type: APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP, 22 | payload: 'payment' 23 | }); 24 | }, 25 | 26 | handlePaymentFormSubmit: (formData, order) => { 27 | formData.order.payments_attributes['0']['amount'] = order.total; 28 | return dispatch (Actions.goToNextStep(order, formData)); 29 | }, 30 | 31 | handleCheckoutStepNotEditable: (order, placedOrder) => { 32 | /* Redirect to last step if order is already placed */ 33 | if (placedOrder.id) { 34 | dispatch (push(APP_ROUTES.checkout[`${ placedOrder.checkout_steps.slice(-1) }PageRoute`])); 35 | } 36 | else { 37 | if (order.id) { 38 | const previousStep = CheckoutStepCalculator.previous(order.checkout_steps, 'payment'); 39 | 40 | dispatch ( push(APP_ROUTES.checkout[`${ previousStep }PageRoute`])); 41 | } 42 | else { 43 | dispatch ( push(APP_ROUTES.cartPageRoute)); 44 | } 45 | } 46 | }, 47 | 48 | showFormErrors: (errorNode) => { 49 | dispatch (Actions.showFlash(errorNode, 'danger')); 50 | } 51 | }; 52 | }; 53 | 54 | const PaymentFormConnector = connect(mapStateToProps, mapDispatchToProps)(PaymentForm); 55 | 56 | export default PaymentFormConnector; 57 | -------------------------------------------------------------------------------- /src/components/styles/core/modal.scss: -------------------------------------------------------------------------------- 1 | .global-modal { 2 | width: $full; 3 | height: $full; 4 | display: none; 5 | position: fixed; 6 | top: 0; 7 | left: 0; 8 | z-index: 999; 9 | text-align: left; 10 | background: rgba(0, 0, 0, .7); 11 | 12 | &.show-modal { 13 | display: block; 14 | } 15 | 16 | .modal-close-button { 17 | position: absolute; 18 | top: 30px; 19 | right: 30px; 20 | color: $color-white; 21 | font-size: 30px; 22 | cursor: pointer; 23 | } 24 | 25 | .global-modal-title { 26 | border-bottom: solid 1px $border-grey; 27 | padding-bottom: 10px; 28 | color: $color-black; 29 | font-size: 24px; 30 | font-weight: 500; 31 | } 32 | 33 | .global-modal-content { 34 | width: $full; 35 | padding-top: 20px; 36 | display: table; 37 | } 38 | 39 | .modal-form-row { 40 | width: $full; 41 | margin-top: 20px; 42 | display: table; 43 | 44 | &:first-child { 45 | margin-top: 0; 46 | } 47 | } 48 | 49 | .modal-form-label { 50 | margin-bottom: 5px; 51 | display: block; 52 | color: $color-black; 53 | font-size: 13px; 54 | font-weight: 400; 55 | } 56 | 57 | .form-input { 58 | width: $full; 59 | height: 40px; 60 | border: solid 1px $border-grey; 61 | padding: 0 10px; 62 | color: $color-black; 63 | font-size: 13px; 64 | background-color: $color-white; 65 | @include transition(all, 0.3s, ease-in-out); 66 | 67 | &:focus { 68 | border-color: $color-black; 69 | } 70 | } 71 | 72 | @media (max-width: 767px) { 73 | .modal-close-button { 74 | top: 15px; 75 | right: 10px; 76 | } 77 | } 78 | } 79 | 80 | .user-login-modal, .user-signup-modal{ 81 | width: 600px; 82 | margin-left: -300px; 83 | padding: 20px 30px; 84 | position: absolute; 85 | top: 150px; 86 | left: 50%; 87 | background: $color-white; 88 | 89 | @media (max-width: 767px) { 90 | width: 94%; 91 | margin-left: 0; 92 | padding: 15px; 93 | top: 50px; 94 | left: 3%; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/components/styles/pages/cart.scss: -------------------------------------------------------------------------------- 1 | .cart-section { 2 | padding: 50px 0; 3 | 4 | .cart-contact-block { 5 | padding-top: 10px; 6 | float: right; 7 | font-size: 13px; 8 | } 9 | 10 | .cart-items-section { 11 | margin-top: 30px; 12 | } 13 | } 14 | 15 | .cart-items-table { 16 | th, 17 | td { 18 | padding: 10px 10px; 19 | 20 | &.cart-item-total-col { 21 | padding-left: 20px; 22 | padding-right: 20px; 23 | } 24 | } 25 | 26 | td { 27 | padding: 20px 10px; 28 | 29 | &.cart-item-image-col { 30 | padding-right: 20px; 31 | } 32 | 33 | &.cart-item-price-col { 34 | padding-right: 20px; 35 | font-size: 16px; 36 | font-weight: 600; 37 | } 38 | 39 | &.cart-item-qty-col { 40 | padding-right: 20px; 41 | white-space: nowrap; 42 | } 43 | 44 | &.cart-item-total-col { 45 | padding-left: 20px; 46 | padding-right: 20px; 47 | font-size: 16px; 48 | font-weight: 600; 49 | } 50 | } 51 | 52 | .line-item { 53 | border-top: solid 1px $border-grey; 54 | 55 | &:nth-child(even) { 56 | background: $light-grey-bg; 57 | } 58 | } 59 | 60 | .cart-img-block { 61 | text-align: center; 62 | } 63 | 64 | .cart-item-details { 65 | padding-top: 10px; 66 | } 67 | 68 | .cart-item-title { 69 | font-size: 18px; 70 | font-weight: 600; 71 | } 72 | 73 | .cart-item-description { 74 | margin-top: 8px; 75 | font-size: 13px; 76 | font-weight: 300; 77 | } 78 | 79 | .cart-item-qty-input { 80 | width: 50px; 81 | height: 30px; 82 | padding: 0 5px; 83 | vertical-align: top; 84 | } 85 | } 86 | 87 | .cart-buttons-row { 88 | margin-top: 30px; 89 | border-top: solid 1px $border-grey; 90 | padding-top: 20px; 91 | 92 | .button-primary { 93 | margin-left: 20px; 94 | 95 | &:first-child { 96 | margin-left: 0; 97 | } 98 | } 99 | } 100 | 101 | .cart-empty-block { 102 | margin-top: 20px; 103 | padding: 30px; 104 | text-align: center; 105 | background: $light-grey-bg; 106 | } 107 | -------------------------------------------------------------------------------- /src/containers/checkout-steps/address-form-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import Actions from '../../actions'; 5 | import APP_ACTIONS from '../../constants/app-actions'; 6 | import AddressForm from '../../components/checkout-steps/address-form'; 7 | import CountryAPI from '../../apis/country'; 8 | 9 | import APP_ROUTES from '../../constants/app-routes'; 10 | 11 | const mapStateToProps = (state, ownProps) => { 12 | return { 13 | order: state.order, 14 | displayLoader: state.displayLoader, 15 | countries: state.countryList.countries, 16 | placedOrder: state.placedOrder 17 | }; 18 | }; 19 | 20 | const mapDispatchToProps = (dispatch) => { 21 | return { 22 | setCurrentCheckoutStep: () => { 23 | dispatch ({ 24 | type: APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP, 25 | payload: 'address' 26 | }); 27 | }, 28 | 29 | handleAddressFormSubmit: (formData, order) => { 30 | return dispatch (Actions.goToNextStep(order, formData)); 31 | }, 32 | 33 | fetchCountries: () => { 34 | dispatch (Actions.displayLoader()); 35 | 36 | CountryAPI.getList().then((response) => { 37 | dispatch (Actions.addCountries(response.body)); 38 | dispatch (Actions.hideLoader()); 39 | }, 40 | (error) => { 41 | dispatch (Actions.showFlash('Unable to connect to server... Please try again later.')); 42 | }) 43 | }, 44 | 45 | handleCheckoutStepNotEditable: (order, placedOrder) => { 46 | /* Redirect to last step if order is already placed */ 47 | if (placedOrder.id) { 48 | dispatch (push(APP_ROUTES.checkout[`${ placedOrder.checkout_steps.slice(-1) }PageRoute`])); 49 | } 50 | else { 51 | dispatch(Actions.showFlash("Your cart is empty!", 'danger')); 52 | dispatch (push(APP_ROUTES.cartPageRoute)); 53 | } 54 | }, 55 | 56 | handleOrderNotPresent: () => { 57 | dispatch (push(APP_ROUTES.cartPageRoute)); 58 | dispatch(Actions.showFlash("Your cart is empty!", 'danger')); 59 | } 60 | }; 61 | }; 62 | 63 | const AddressFormConnector = connect(mapStateToProps, mapDispatchToProps)(AddressForm); 64 | 65 | export default AddressFormConnector; 66 | -------------------------------------------------------------------------------- /src/services/checkout-step-calculator.js: -------------------------------------------------------------------------------- 1 | const CheckoutStepCalculator = { 2 | next: (checkoutSteps, currentStep) => { 3 | if (CheckoutStepCalculator.isLastStep(checkoutSteps, currentStep)) { 4 | return ""; 5 | } 6 | else { 7 | let indexOfCurrentStep = checkoutSteps.indexOf(currentStep); 8 | return checkoutSteps[indexOfCurrentStep + 1]; 9 | } 10 | }, 11 | 12 | previous: (checkoutSteps, currentStep) => { 13 | if (CheckoutStepCalculator.isFirstStep(checkoutSteps, currentStep)) { 14 | return 'cart'; 15 | } 16 | else { 17 | let indexOfCurrentStep = checkoutSteps.indexOf(currentStep); 18 | return checkoutSteps[indexOfCurrentStep - 1]; 19 | } 20 | }, 21 | 22 | isPristineStep: (checkoutSteps, currentStep, orderState) => { 23 | if (CheckoutStepCalculator.isLastStep(checkoutSteps, currentStep)) { 24 | return true; 25 | } 26 | else { 27 | let indexOfCurrentStep = checkoutSteps.indexOf(currentStep); 28 | let indexOfOrderState = checkoutSteps.indexOf(orderState); 29 | 30 | return indexOfCurrentStep >= indexOfOrderState; 31 | } 32 | }, 33 | 34 | isStepEditable: (checkoutSteps, currentStep, orderState) => { 35 | let indexOfCurrentStep = checkoutSteps.indexOf(currentStep); 36 | let indexOfOrderState = checkoutSteps.indexOf(orderState); 37 | 38 | return !CheckoutStepCalculator.isLastStep(checkoutSteps, orderState) && 39 | CheckoutStepCalculator.isValidStep(checkoutSteps, currentStep) && 40 | ( currentStep === orderState || 41 | indexOfOrderState >= indexOfCurrentStep ); 42 | }, 43 | 44 | isLastStep: (checkoutSteps, currentStep) => { 45 | let indexOfCurrentStep = checkoutSteps.indexOf(currentStep); 46 | 47 | return (CheckoutStepCalculator.isValidStep(checkoutSteps, currentStep) && 48 | indexOfCurrentStep === (checkoutSteps.length - 1)); 49 | }, 50 | 51 | isFirstStep: (checkoutSteps, currentStep) => { 52 | let indexOfCurrentStep = checkoutSteps.indexOf(currentStep); 53 | 54 | return (indexOfCurrentStep === 0); 55 | }, 56 | 57 | isValidStep: (checkoutSteps, currentStep) => { 58 | return (checkoutSteps.indexOf(currentStep) !== -1); 59 | } 60 | 61 | } 62 | 63 | export default CheckoutStepCalculator; 64 | -------------------------------------------------------------------------------- /src/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | 4 | import HomePageConnector from './containers/home-page-connector'; 5 | import ProductShowConnector from './containers/product/product-show-connector'; 6 | import CartShowConnector from './containers/cart/cart-show-connector'; 7 | 8 | import AddressFormConnector from './containers/checkout-steps/address-form-connector'; 9 | import DeliveryFormConnector from './containers/checkout-steps/delivery-form-connector'; 10 | import PaymentFormConnector from './containers/checkout-steps/payment-form-connector'; 11 | import CheckoutConfirmationConnector from './containers/checkout-steps/confirmation-form-connector'; 12 | import CheckoutSuccessConnector from './containers/checkout-steps/checkout-success-connector'; 13 | import OrderListConnector from './containers/order/list-connector'; 14 | import OrderShowConnector from './containers/order/show-connector'; 15 | 16 | import APP_ROUTES from './constants/app-routes'; 17 | 18 | export default function configRoutes() { 19 | return ( 20 | 21 |
    22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
    35 |
    36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | var nodePaths = (process.env.NODE_PATH || '') 23 | .split(process.platform === 'win32' ? ';' : ':') 24 | .filter(Boolean) 25 | .map(resolveApp); 26 | 27 | // config after eject: we're in ./config/ 28 | module.exports = { 29 | appBuild: resolveApp('build'), 30 | appPublic: resolveApp('public'), 31 | appHtml: resolveApp('public/index.html'), 32 | appIndexJs: resolveApp('src/index.js'), 33 | appPackageJson: resolveApp('package.json'), 34 | appSrc: resolveApp('src'), 35 | testsSetup: resolveApp('src/setupTests.js'), 36 | appNodeModules: resolveApp('node_modules'), 37 | ownNodeModules: resolveApp('node_modules'), 38 | nodePaths: nodePaths 39 | }; 40 | 41 | 42 | 43 | // config before publish: we're in ./packages/react-scripts/config/ 44 | if (__dirname.indexOf(path.join('packages', 'react-scripts', 'config')) !== -1) { 45 | module.exports = { 46 | appBuild: resolveOwn('../../../build'), 47 | appPublic: resolveOwn('../template/public'), 48 | appHtml: resolveOwn('../template/public/index.html'), 49 | appIndexJs: resolveOwn('../template/src/index.js'), 50 | appPackageJson: resolveOwn('../package.json'), 51 | appSrc: resolveOwn('../template/src'), 52 | testsSetup: resolveOwn('../template/src/setupTests.js'), 53 | appNodeModules: resolveOwn('../node_modules'), 54 | ownNodeModules: resolveOwn('../node_modules'), 55 | nodePaths: nodePaths 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /src/components/order/panel-view.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | // import { Panel } from 'react-bootstrap'; 3 | import { FormattedMessage } from 'react-intl'; 4 | 5 | import Shipment from './shipment'; 6 | 7 | class OrderPanelView extends Component { 8 | render() { 9 | let thisOrder = this.props.order; 10 | let shipmentsMarkup = thisOrder.shipments.map((shipment, idx) => { 11 | return ( 12 | 17 | ); 18 | }); 19 | 20 | return ( 21 |
    22 |
    23 | { shipmentsMarkup } 24 |
    25 |
    26 | ); 27 | }; 28 | 29 | _panelHeaderMarkup() { 30 | let thisOrder = this.props.order; 31 | let paymentStatus = thisOrder.payment_state === 'paid' ? 'success' : 'danger'; 32 | 33 | return ( 34 |
    35 |
    36 | 42 | 49 | 55 | 56 | 60 | : { thisOrder.payment_state } 61 | 62 |
    63 |
    64 | ); 65 | } 66 | }; 67 | 68 | export default OrderPanelView; 69 | -------------------------------------------------------------------------------- /src/components/product/image-viewer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import ProductImagePreview from './image-preview'; 4 | import ThumbnailList from './thumbnail-list'; 5 | 6 | import Loader from '../shared/loader'; 7 | 8 | class ImageViewer extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.handleImageLoad = this.handleImageLoad.bind(this) 12 | this.onClickThumbnail = this.onClickThumbnail.bind(this) 13 | this.onMouseOutThumbnail = this.onMouseOutThumbnail.bind(this) 14 | this.onMouseOverThumbnail = this.onMouseOverThumbnail.bind(this) 15 | this.state = { 16 | displayImageLoader: true, 17 | previewImageNo: 0, 18 | currentImageNo: 0 19 | } 20 | }; 21 | 22 | handleImageLoad(){ 23 | this.setState({ 24 | displayImageLoader: false 25 | }) 26 | }; 27 | 28 | onClickThumbnail(imageNo){ 29 | this.setState({ 30 | previewImageNo: imageNo, 31 | currentImageNo: imageNo 32 | }) 33 | } 34 | 35 | onMouseOverThumbnail(imageNo){ 36 | this.setState({ 37 | previewImageNo: imageNo, 38 | }) 39 | } 40 | 41 | onMouseOutThumbnail(){ 42 | this.setState({ 43 | previewImageNo: this.state.currentImageNo, 44 | }) 45 | } 46 | 47 | render(){ 48 | let productImages = []; 49 | let previewImage = {}; 50 | let returnString = null; 51 | if (this.props.productVariant){ 52 | productImages = this.props.productVariant.images; 53 | previewImage = this.props.productVariant.images[this.state.previewImageNo]; 54 | returnString =
    55 |
    56 | 57 | 58 |
    59 | 60 |
    61 | 65 |
    66 |
    67 | } 68 | return( 69 | returnString 70 | ); 71 | } 72 | }; 73 | 74 | export default ImageViewer; 75 | -------------------------------------------------------------------------------- /src/containers/cart/cart-show-connector.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux'; 2 | import { push } from 'react-router-redux'; 3 | 4 | import CartShow from '../../components/cart/show'; 5 | import Actions from '../../actions'; 6 | import APP_ACTIONS from '../../constants/app-actions'; 7 | 8 | import APP_ROUTES from '../../constants/app-routes'; 9 | 10 | const mapStateToProps = (state, ownProps) => { 11 | return { 12 | order: state.order 13 | }; 14 | }; 15 | 16 | const mapDispatchToProps = (dispatch) => { 17 | return { 18 | setCurrentCheckoutStep: () => { 19 | dispatch ({ 20 | type: APP_ACTIONS.SET_CURRENT_CHECKOUT_STEP, 21 | payload: 'cart' 22 | }); 23 | }, 24 | 25 | emptyCart: (order) => { 26 | if (order.id) { 27 | dispatch (Actions.emptyCart(order)).then((response) => { 28 | dispatch (Actions.showFlash('Your Cart is now empty!!')); 29 | }, 30 | (error) => { 31 | dispatch (Actions.showFlash('Sorry! We were unable to empty your cart. Please try again later.', 'danger')); 32 | }); 33 | } 34 | else{ 35 | dispatch (Actions.showFlash('Your Cart is already empty!!')); 36 | } 37 | }, 38 | 39 | destroyLineItem: (lineItem) => { 40 | dispatch (Actions.removeProductFromCart(lineItem.id)).then(response => { 41 | dispatch (Actions.refreshOrder()); 42 | dispatch (Actions.showFlash('Line Item removed from the cart.')); 43 | }, 44 | (error) => { 45 | dispatch (Actions.showFlash('Unable to remove line item from cart!!', 'danger')); 46 | }); 47 | }, 48 | 49 | changeQuantity: (lineItemId, quantity) => { 50 | if (parseInt(quantity, 10) > 0) { 51 | dispatch (Actions.changeProductQuantityFromCart(quantity, lineItemId)).then((response) => { 52 | dispatch (Actions.refreshOrder()); 53 | dispatch (Actions.showFlash('Your Product Quantity is successfully updated!!')); 54 | }, 55 | (error) => { 56 | dispatch (Actions.showFlash('Unable to update the product quantity', 'danger')); 57 | }); 58 | } 59 | else { 60 | dispatch (Actions.showFlash('Quantity must be numeric and greater than zero.', 'danger')); 61 | } 62 | }, 63 | 64 | doCheckout: (order) => { 65 | if (order.state === 'cart') { 66 | dispatch (Actions.goToNextStep(order)); 67 | } 68 | else { 69 | dispatch (push(APP_ROUTES.checkout.addressPageRoute)); 70 | } 71 | } 72 | 73 | }; 74 | }; 75 | 76 | const CartShowConnector = connect(mapStateToProps, mapDispatchToProps)(CartShow); 77 | 78 | export default CartShowConnector; 79 | -------------------------------------------------------------------------------- /src/components/product-tile.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import URLSanitizer from '../services/url-sanitizer'; 5 | 6 | import styles from './styles/components/product-tile.scss'; 7 | 8 | class ProductTile extends Component { 9 | constructor(props){ 10 | super(props); 11 | 12 | this.addProductToCart = this.addProductToCart.bind(this); 13 | }; 14 | 15 | addProductToCart () { 16 | this.props.addProductToCart(this.props.product.master.id, 1); 17 | }; 18 | 19 | _addToCartMarkup () { 20 | let addToCartMarkup; 21 | 22 | if (this.props.productInCart) { 23 | addToCartMarkup = 24 | 25 | ; 26 | } 27 | else { 28 | addToCartMarkup = 29 | 30 | ; 31 | } 32 | 33 | return addToCartMarkup; 34 | }; 35 | 36 | render() { 37 | let image = this.props.product.master.images[0] || {}; 38 | let productName = this.props.product.name; 39 | let productShowURL = '/products/' + this.props.product.slug; 40 | let imageUrl = URLSanitizer.makeAbsolute(image.product_url); 41 | return ( 42 |
    43 |
    44 |
    45 | 46 | { 49 | 50 | 51 | 52 | 56 |
    57 | 58 | 59 |
    60 |
    61 | 62 | { productName } 63 | 64 |
    65 | ${ this.props.product.master.price } 66 |
    67 | 68 |
    69 |
    70 | ); 71 | }; 72 | }; 73 | 74 | export default ProductTile; 75 | -------------------------------------------------------------------------------- /src/apis/order.js: -------------------------------------------------------------------------------- 1 | var request = require('superagent'); 2 | import SpreeAPIOrderAdapter from './ams-adapters/spree-api-order-adapter'; 3 | 4 | const OrdersAPI = { 5 | getItem: (params) => { 6 | return request 7 | .get(`${process.env.REACT_APP_AMS_API_BASE}/orders/${ params.orderNumber }`) 8 | .query(params.tokenParam) 9 | .set('Accept', 'application/json') 10 | .send() 11 | .then((response) => { 12 | let processedResponse = SpreeAPIOrderAdapter.processItem(response.body); 13 | response.body = processedResponse; 14 | 15 | return response; 16 | }); 17 | }, 18 | 19 | mine: (apiToken) => { 20 | return request 21 | .get(`${process.env.REACT_APP_AMS_API_BASE}/orders/mine`) 22 | .query({ token: apiToken, 'q[state_cont]': 'complete' }) 23 | .set('Accept', 'application/json') 24 | .send() 25 | .then((response) => { 26 | let processedResponse = SpreeAPIOrderAdapter.processList(response.body); 27 | response.body = processedResponse; 28 | 29 | return response; 30 | }); 31 | }, 32 | 33 | create: (params) => { 34 | return request 35 | .post(`${process.env.REACT_APP_AMS_API_BASE}/orders`) 36 | .query(params) 37 | .set('Accept', 'application/json') 38 | .send() 39 | .then((response) => { 40 | let processedResponse = SpreeAPIOrderAdapter.processItem(response.body); 41 | response.body = processedResponse; 42 | 43 | return response; 44 | }); 45 | }, 46 | 47 | destroy: (params) => { 48 | return request 49 | .put(`${process.env.REACT_APP_AMS_API_BASE}/orders/${params.orderNumber}/empty`) 50 | .query(params.tokenParam) 51 | .set('Accept', 'application/json'); 52 | }, 53 | 54 | update: (orderNumber, tokenParam, params = {}) => { 55 | return request 56 | .put(`${process.env.REACT_APP_AMS_API_BASE}/orders/${orderNumber}`) 57 | .query(tokenParam) 58 | .set('Accept', 'application/json') 59 | .send(params) 60 | .then((response) => { 61 | let processedResponse = SpreeAPIOrderAdapter.processItem(response.body); 62 | response.body = processedResponse; 63 | 64 | return response; 65 | }); 66 | }, 67 | 68 | getCurrent: (userAPIToken) => { 69 | return request 70 | .get(`${process.env.REACT_APP_AMS_API_BASE}/orders/current`) 71 | .query({ token: userAPIToken }) 72 | .set('Accept', 'application/json') 73 | .send() 74 | .then((response) => { 75 | /* 76 | Return response with empty body if no current order was found. 77 | */ 78 | if (response.body === null) { 79 | return { body: {} }; 80 | } 81 | 82 | let processedResponse = SpreeAPIOrderAdapter.processItem(response.body); 83 | response.body = processedResponse; 84 | 85 | return response; 86 | }); 87 | } 88 | } 89 | 90 | export default OrdersAPI; 91 | -------------------------------------------------------------------------------- /src/images/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/checkout-steps/delivery/shipment.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Field } from 'redux-form'; 3 | import { FormattedMessage } from 'react-intl'; 4 | 5 | import LineItem from '../../order/line-item'; 6 | 7 | class Shipment extends Component { 8 | constructor(props) { 9 | super(props); 10 | this.renderHiddenFieldForShipmentId = this.renderHiddenFieldForShipmentId.bind(this); 11 | }; 12 | 13 | componentDidMount () { 14 | this.setShipmentIdInHiddenField(this.props.shipment.id); 15 | }; 16 | 17 | renderHiddenFieldForShipmentId (field) { 18 | this.onChangeCallbackForShipmentId = field.input.onChange; 19 | return 20 | }; 21 | 22 | /* 23 | :DIRTY HACK: 24 | Redux-form form doesn't support hidden fields. So, we create a hidden field 25 | and manually trigger its onChange to set the value in redux-form reducer. 26 | */ 27 | setShipmentIdInHiddenField(value) { 28 | this.onChangeCallbackForShipmentId(value); 29 | }; 30 | 31 | _shipmentLineItemsMarkup() { 32 | let thisShipment = this.props.shipment; 33 | 34 | let shipmentLineItems = this.props.orderLineItems.filter((lineItem) => { 35 | return thisShipment.line_item_ids.indexOf(lineItem.id) !== -1; 36 | }); 37 | 38 | return shipmentLineItems.map((lineItem, idx) => { 39 | return 40 | }); 41 | }; 42 | 43 | render() { 44 | let shipment = this.props.shipment; 45 | 46 | let shipmentMarkup = shipment.shipping_rates.map((shippingRate, idx) => { 47 | let label = `${ shippingRate.name }( ${ shippingRate.display_cost } )`; 48 | return ( 49 |
    50 | 58 |
    59 | ); 60 | }); 61 | 62 | return ( 63 |
    64 |
    65 | { `Shipment - ${ this.props.shipmentIndex }` } 66 |
    67 |

    68 | 72 |

    73 | { this._shipmentLineItemsMarkup() } 74 |
    75 | { shipmentMarkup } 76 |
    77 | 78 | 79 |
    80 | ); 81 | }; 82 | }; 83 | 84 | export default Shipment; 85 | -------------------------------------------------------------------------------- /src/components/checkout-steps/base-checkout-layout.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link } from 'react-router-dom'; 3 | 4 | import Loader from "../shared/loader" 5 | import APP_ROUTES from '../../constants/app-routes'; 6 | 7 | import OrderSummaryConnector from '../../containers/order/summary-connector'; 8 | 9 | class BaseCheckoutLayout extends Component { 10 | 11 | render() { 12 | this.checkoutStepsMarkup = []; 13 | this.__generateCheckoutStepsMarkup(this.props.currentStep); 14 | 15 | return ( 16 |
    17 | 18 |
    19 |
    20 |
    21 | { this.checkoutStepsMarkup } 22 |
    23 |
    24 | 25 |
    26 |
    27 |
    28 |
    29 | ); 30 | }; 31 | 32 | /* 33 | This function iterates over all the steps and calls another function for 34 | generating markup for each step. 35 | */ 36 | __generateCheckoutStepsMarkup (currentStep) { 37 | this.props.checkoutSteps.forEach((checkoutStep) => { 38 | let titleizedStepName = checkoutStep[0].toUpperCase() + checkoutStep.substr(1).toLowerCase(); 39 | this.__generateMarkupForStep(currentStep, checkoutStep.trim(), `${ titleizedStepName }`); 40 | }); 41 | }; 42 | 43 | /* 44 | This function generates two things, namely, the title section for each 45 | checkout step and the content for the checkout step body (actual form). 46 | It then pushes the result into +checkoutStepsMarkup+. 47 | */ 48 | __generateMarkupForStep (currentStep, thisStep, title) { 49 | thisStep = thisStep.trim(); 50 | 51 | 52 | const innerHtml = this.__generateMarkupForStepBody(currentStep, thisStep, title); 53 | const formattedTitle = 54 | { title } 55 | ; 56 | 57 | this.checkoutStepsMarkup.push ( 58 |
    59 |

    60 | { formattedTitle } 61 |

    62 | { innerHtml } 63 |
    64 | ); 65 | }; 66 | 67 | /* 68 | This generates mark up for step body(form fields for the step etc) if the 69 | +thisStep+ is same as the +currentStep+. It doesn't generate markup for the 70 | step title. 71 | */ 72 | __generateMarkupForStepBody (currentStep, thisStep, title) { 73 | if ( currentStep === thisStep ) { 74 | return ( 75 |
    76 | { this.props.children } 77 |
    78 | ); 79 | } 80 | else { 81 | return null; 82 | } 83 | }; 84 | 85 | }; 86 | 87 | export default BaseCheckoutLayout; 88 | -------------------------------------------------------------------------------- /src/components/taxon-filters/styles/filter-bar.scss: -------------------------------------------------------------------------------- 1 | .width300 { 2 | width: 300px; 3 | } 4 | 5 | .headerNavContainer { 6 | width: 100%; 7 | margin: 0; 8 | margin-left: -10px; 9 | padding: 0; 10 | display: table; 11 | } 12 | 13 | .mainNavHolder { 14 | height: 25px; 15 | margin-left: 20px; 16 | display: inline-block; 17 | 18 | &:hover { 19 | > a { 20 | color: #fff; 21 | background: #4eaf79; 22 | } 23 | 24 | > ul { 25 | display: block; 26 | 27 | > li { 28 | a { 29 | background: none; 30 | } 31 | } 32 | } 33 | } 34 | 35 | a { 36 | padding: 10px; 37 | font-family: 'Roboto', sans-serif; 38 | font-size: 13px; 39 | font-weight: 500; 40 | } 41 | 42 | .mainNavHolder { 43 | width: 100%; 44 | margin-left: 0; 45 | } 46 | 47 | &:first-child { 48 | margin-left: 0; 49 | } 50 | 51 | > ul { 52 | border: none; 53 | border-radius: 0; 54 | top: 23px; 55 | background: #4eaf79; 56 | 57 | /*&:before { 58 | content: ''; 59 | width: 10px; 60 | height: 10px; 61 | border: none; 62 | border-top: solid 1px #c7c7c7; 63 | border-left: solid 1px #c7c7c7; 64 | position: absolute; 65 | top: -6px; 66 | left: 10px; 67 | background: #fff; 68 | -webkit-transform: rotate(45deg); 69 | -moz-transform: rotate(45deg); 70 | transform: rotate(45deg); 71 | }*/ 72 | 73 | > li { 74 | padding: 0 10px; 75 | 76 | > a { 77 | border-top: solid 1px #259557; 78 | padding: 8px 0; 79 | font-size: 12px; 80 | 81 | &:hover { 82 | color: #fff; 83 | background: none; 84 | } 85 | 86 | > span { 87 | border-top: solid 4px transparent; 88 | border-bottom: solid 4px transparent; 89 | border-left: solid 4px #000; 90 | border-right: none; 91 | } 92 | } 93 | 94 | > ul { 95 | border: none; 96 | top: 0; 97 | left: 100%; 98 | border-radius: 0; 99 | 100 | &:before { 101 | top: 10px; 102 | left: -6px; 103 | -webkit-transform: rotate(-45deg); 104 | -moz-transform: rotate(-45deg); 105 | transform: rotate(-45deg); 106 | } 107 | 108 | > li { 109 | padding: 0 10px; 110 | 111 | > a { 112 | border-top: solid 1px #E1E1E1; 113 | padding: 8px 0; 114 | font-size: 12px; 115 | background: none; 116 | 117 | &:hover { 118 | color: #fff; 119 | background: none; 120 | } 121 | } 122 | 123 | &:first-child { 124 | > a { 125 | border-top: none; 126 | } 127 | } 128 | } 129 | } 130 | 131 | &:first-child { 132 | > a { 133 | border-top: none; 134 | } 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/components/order/summary.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Panel, Table } from 'react-bootstrap'; 3 | import { FormattedMessage, injectIntl } from 'react-intl'; 4 | 5 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 6 | 7 | class OrderSummary extends Component { 8 | 9 | mapPropertiesToTable () { 10 | let orderPropertiesMapper = this.getOrderPropertiesMapper(); 11 | return orderPropertiesMapper.map((property, idx) => { 12 | return ( 13 | 14 | 15 | 18 | 19 | 20 | { property[1] } 21 | 22 | 23 | ) 24 | }); 25 | } 26 | 27 | getOrderPropertiesMapper () { 28 | let thisOrder = this.props.order; 29 | const ItemTotalRep = ; 33 | const adjustmentTotalRep = ; 37 | const shippingTotalRep = ; 41 | const orderTotalRep = 45 | 46 | if (this.props.placedOrder && this.props.placedOrder.id) { 47 | thisOrder = this.props.placedOrder; 48 | } 49 | 50 | let orderPropertiesMapper = []; 51 | let isValidStep = CheckoutStepCalculator.isValidStep(thisOrder.checkout_steps, this.props.currentCheckoutStep); 52 | // debugger 53 | if (isValidStep) { 54 | orderPropertiesMapper.push( 55 | [ItemTotalRep, `$${thisOrder.item_total}`], 56 | [adjustmentTotalRep, `$${thisOrder.adjustment_total}`] 57 | ); 58 | }; 59 | 60 | if (thisOrder.checkout_steps.indexOf(this.props.currentCheckoutStep) >= 2 ) { 61 | orderPropertiesMapper.push( 62 | [shippingTotalRep, `$${thisOrder.shipment_total}`] 63 | ); 64 | }; 65 | orderPropertiesMapper.push( 66 | [orderTotalRep, `$${thisOrder.total}`] 67 | ); 68 | return orderPropertiesMapper; 69 | } 70 | 71 | render() { 72 | return ( 73 |
    74 | 76 | 77 | 78 | {this.mapPropertiesToTable()} 79 | 80 |
    81 |
    82 |
    83 | ); 84 | }; 85 | 86 | }; 87 | 88 | export default injectIntl(OrderSummary); 89 | -------------------------------------------------------------------------------- /src/components/checkout-steps/delivery-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { reduxForm } from 'redux-form'; 4 | import { FormattedMessage } from 'react-intl'; 5 | 6 | import Layout from "../layout"; 7 | import BaseCheckoutLayout from "./base-checkout-layout"; 8 | import Shipment from './delivery/shipment'; 9 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 10 | 11 | class DeliveryForm extends Component { 12 | 13 | /* Render this step only if order is present and in a valid checkout state. */ 14 | componentWillMount() { 15 | let order = this.props.order; 16 | 17 | if (!CheckoutStepCalculator.isStepEditable(order.checkout_steps, 'delivery', order.state)){ 18 | this.props.handleCheckoutStepNotEditable(order, this.props.placedOrder); 19 | } 20 | }; 21 | 22 | componentDidMount() { 23 | this.props.setCurrentCheckoutStep(); 24 | }; 25 | 26 | handleDeliveryFormSubmit (formData) { 27 | this.props.handleDeliveryFormSubmit(formData, this.props.order); 28 | }; 29 | 30 | render() { 31 | let shipments = this.props.order.shipments; 32 | const { handleSubmit, valid, submitting } = this.props; 33 | 34 | let shipmentsMarkup = shipments.map((shipment, idx) => { 35 | return ( 36 | 41 | ); 42 | }); 43 | 44 | return ( 45 | 46 | 49 |
    50 |
    51 | { shipmentsMarkup } 52 |
    53 | 54 |
    55 | 63 |
    64 |
    65 |
    66 |
    67 | ); 68 | }; 69 | }; 70 | 71 | DeliveryForm = reduxForm({ 72 | form: 'deliveryForm' 73 | })(DeliveryForm); 74 | 75 | DeliveryForm = connect( 76 | state => { 77 | const shipments = state.order.shipments || []; 78 | const shipments_attributes = {}; 79 | 80 | shipments.forEach((shipment, idx) => { 81 | shipments_attributes[idx] = { selected_shipping_rate_id: `${shipment.selected_shipping_rate.id}` } 82 | }); 83 | 84 | return { 85 | initialValues: { 86 | order: { 87 | shipments_attributes 88 | } 89 | } 90 | }; 91 | } 92 | )(DeliveryForm) 93 | 94 | export default DeliveryForm; 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spree-on-react", 3 | "author": "Shubham Gupta", 4 | "license": "MIT", 5 | "version": "0.1.0", 6 | "private": true, 7 | "devDependencies": { 8 | "autoprefixer": "6.5.1", 9 | "babel-cli": "^6.24.1", 10 | "babel-core": "6.17.0", 11 | "babel-eslint": "7.0.0", 12 | "babel-jest": "16.0.0", 13 | "babel-loader": "6.2.5", 14 | "babel-preset-react-app": "^1.0.0", 15 | "case-sensitive-paths-webpack-plugin": "1.1.4", 16 | "chalk": "1.1.3", 17 | "connect-history-api-fallback": "1.3.0", 18 | "cross-spawn": "4.0.2", 19 | "css-loader": "0.25.0", 20 | "deepmerge": "^1.4.1", 21 | "detect-port": "1.0.1", 22 | "dotenv": "2.0.0", 23 | "eslint": "3.8.1", 24 | "eslint-config-react-app": "^0.3.0", 25 | "eslint-loader": "1.6.0", 26 | "eslint-plugin-flowtype": "2.21.0", 27 | "eslint-plugin-import": "2.0.1", 28 | "eslint-plugin-jsx-a11y": "2.2.3", 29 | "eslint-plugin-react": "6.4.1", 30 | "extract-text-webpack-plugin": "1.0.1", 31 | "file-loader": "0.9.0", 32 | "filesize": "3.3.0", 33 | "find-cache-dir": "0.1.1", 34 | "flat": "^2.0.1", 35 | "fs-extra": "0.30.0", 36 | "gzip-size": "3.0.0", 37 | "html-webpack-plugin": "2.24.0", 38 | "http-proxy-middleware": "0.17.2", 39 | "jest": "16.0.2", 40 | "json-loader": "0.5.4", 41 | "node-sass": "^3.13.0", 42 | "object-assign": "4.1.0", 43 | "path-exists": "2.1.0", 44 | "postcss-loader": "1.0.0", 45 | "promise": "7.1.1", 46 | "react-dev-utils": "^0.3.0", 47 | "react-router-dom": "^4.1.1", 48 | "recursive-readdir": "2.1.0", 49 | "redux-logger": "^2.7.4", 50 | "rimraf": "2.5.4", 51 | "sass-loader": "^4.0.2", 52 | "strip-ansi": "3.0.1", 53 | "style-loader": "0.13.1", 54 | "url-loader": "0.5.7", 55 | "webpack": "1.13.2", 56 | "webpack-dev-server": "1.16.2", 57 | "webpack-manifest-plugin": "1.1.0", 58 | "whatwg-fetch": "1.0.0" 59 | }, 60 | "dependencies": { 61 | "bootstrap": "^3.3.7", 62 | "history": "^4.6.3", 63 | "prop-types": "^15.5.10", 64 | "react": "^15.3.2", 65 | "react-boostrap-carousel": "^2.0.4", 66 | "react-bootstrap": "^0.30.6", 67 | "react-bootstrap-dropdown": "^0.3.0", 68 | "react-dom": "^15.3.2", 69 | "react-intl": "^2.3.0", 70 | "react-redux": "^4.4.6", 71 | "react-router": "^3.0.0", 72 | "react-router-redux": "^5.0.0-alpha.6", 73 | "redux": "^3.6.0", 74 | "redux-form": "^6.2.1", 75 | "redux-infinite-scroll": "^1.0.9", 76 | "redux-reset": "^0.2.0", 77 | "redux-thunk": "^2.1.0", 78 | "superagent": "^2.3.0" 79 | }, 80 | "scripts": { 81 | "start": "node scripts/start.js", 82 | "build:langs": "./node_modules/.bin/babel-node scripts/translate.js", 83 | "build": "npm run build:langs && node scripts/build.js", 84 | "test": "node scripts/test.js --env=jsdom" 85 | }, 86 | "jest": { 87 | "moduleFileExtensions": [ 88 | "jsx", 89 | "js", 90 | "json" 91 | ], 92 | "moduleNameMapper": { 93 | "^.+\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "/config/jest/FileStub.js", 94 | "^.+\\.css$": "/config/jest/CSSStub.js" 95 | }, 96 | "setupFiles": [ 97 | "/config/polyfills.js" 98 | ], 99 | "testPathIgnorePatterns": [ 100 | "/(build|docs|node_modules)/" 101 | ], 102 | "testEnvironment": "node" 103 | }, 104 | "babel": { 105 | "presets": [ 106 | "react-app" 107 | ] 108 | }, 109 | "eslintConfig": { 110 | "extends": "react-app" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/components/styles/pages/checkout.scss: -------------------------------------------------------------------------------- 1 | .checkout-section { 2 | padding: 50px 0; 3 | 4 | .checkout-block-content { 5 | margin-top: 30px; 6 | padding: 25px; 7 | background: $light-grey-bg; 8 | } 9 | 10 | .checkout-title-block { 11 | margin: 0 0 30px; 12 | } 13 | 14 | .checkout-section-title { 15 | margin-top: 40px; 16 | border-bottom: solid 1px $border-grey; 17 | padding-bottom: 8px; 18 | font-size: 16px; 19 | font-weight: 600; 20 | 21 | &:first-child { 22 | margin-top: 0; 23 | } 24 | } 25 | 26 | .checkout-section-message { 27 | margin: 10px 0; 28 | } 29 | 30 | .checkout-form-row { 31 | margin-top: 25px; 32 | 33 | &.checkbox-row { 34 | label { 35 | display: inline-block; 36 | } 37 | 38 | .checkout-form-fields { 39 | margin-right: 10px; 40 | float: left; 41 | } 42 | } 43 | } 44 | 45 | .checkout-form-label { 46 | margin-bottom: 3px; 47 | display: block; 48 | color: $dark-grey-text; 49 | font-size: 13px; 50 | font-weight: 400; 51 | } 52 | 53 | .checkout-line-item-row { 54 | margin: 0; 55 | border-bottom: solid 1px $border-grey; 56 | padding: 15px 0; 57 | } 58 | 59 | .checkout-line-item-image-block { 60 | text-align: center; 61 | } 62 | 63 | .checkout-line-item-options-block { 64 | margin-top: 15px; 65 | } 66 | 67 | .checkout-line-item-options { 68 | padding: 10px 0; 69 | 70 | input[type="radio"] { 71 | margin-right: 10px; 72 | } 73 | } 74 | 75 | .checkout-payment-options { 76 | margin-right: 25px; 77 | display: inline-block; 78 | 79 | input[type="radio"] { 80 | margin-right: 10px; 81 | } 82 | } 83 | 84 | .checkout-payment-button-block { 85 | margin-top: 20px; 86 | } 87 | } 88 | 89 | .checkout-confirmation-block { 90 | .confirmation-success-message { 91 | display: block; 92 | font-size: 16px; 93 | } 94 | 95 | .confirmation-details-block { 96 | margin-top: 20px; 97 | } 98 | 99 | .shipment-heading { 100 | strong { 101 | display: block; 102 | } 103 | } 104 | 105 | .label-block-row { 106 | margin-top: 5px; 107 | 108 | label { 109 | margin-right: 12px; 110 | padding: 0; 111 | display: inline-block; 112 | color: $color-black; 113 | 114 | &.label-default { 115 | padding: 5px; 116 | position: relative; 117 | color: $color-white; 118 | } 119 | } 120 | } 121 | 122 | .checkout-shipment-order-total { 123 | margin-top: 10px; 124 | font-size: 16px; 125 | font-weight: 600; 126 | 127 | small { 128 | margin-right: 10px; 129 | font-size: 14px; 130 | font-weight: 400; 131 | } 132 | } 133 | 134 | .order-header-labels { 135 | margin-top: 5px; 136 | color: $color-black; 137 | 138 | .label { 139 | margin-right: 10px; 140 | padding: 0; 141 | color: $color-black; 142 | 143 | &.label-default { 144 | padding: 5px 10px; 145 | color: $color-white; 146 | } 147 | } 148 | } 149 | } 150 | 151 | .checkout-order-details { 152 | .panel { 153 | padding: 15px; 154 | @include border-radius(0); 155 | @include box-shadow(0, 0, 0, transparent); 156 | } 157 | 158 | .panel-heading { 159 | padding: 0 0 10px; 160 | background: transparent; 161 | } 162 | 163 | table.table { 164 | tr { 165 | &:first-child { 166 | td { 167 | border-top: none; 168 | } 169 | } 170 | } 171 | 172 | td { 173 | padding-left: 0; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/components/user-login.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { reduxForm, Field } from 'redux-form'; 3 | import { FormattedMessage } from 'react-intl'; 4 | 5 | import Modal from './shared/modal'; 6 | import FlashConnector from '../containers/flash-connector'; 7 | 8 | class userLogin extends Component { 9 | constructor(props){ 10 | super(props); 11 | 12 | this.handleFormSubmit = this.handleFormSubmit.bind(this); 13 | this.closeModal = this.closeModal.bind(this); 14 | }; 15 | 16 | handleFormSubmit (formData) { 17 | this.props.submitLoginForm(formData).then((response) => { 18 | this.closeModal(); 19 | }, 20 | (error) => {}); 21 | }; 22 | 23 | closeModal () { 24 | this.props.closeModal(); 25 | /* Reset the redux form when modal is closed */ 26 | this.props.reset(); 27 | }; 28 | 29 | render() { 30 | const { handleSubmit, valid, submitting } = this.props; 31 | 32 | return ( 33 | 34 |
    35 |
    36 |
    37 |

    38 | 42 |

    43 |
    44 | 45 |
    46 |
    47 | 53 |
    54 | 58 |
    59 |
    60 | 61 |
    62 | 68 |
    69 | 73 |
    74 |
    75 | 76 |
    77 |
    78 | 86 |
    87 |
    88 |
    89 |
    90 |
    91 |
    92 |
    93 |
    94 | ); 95 | }; 96 | }; 97 | 98 | userLogin = reduxForm({ 99 | form: 'userLogin' 100 | })(userLogin); 101 | 102 | export default userLogin; 103 | -------------------------------------------------------------------------------- /src/components/checkout-steps/confirmation-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { reduxForm } from 'redux-form'; 3 | import { FormattedMessage } from 'react-intl'; 4 | 5 | import Layout from "../layout"; 6 | import BaseCheckoutLayout from "./base-checkout-layout"; 7 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 8 | 9 | import Address from '../order/address'; 10 | import LineItem from '../order/line-item'; 11 | 12 | class ConfirmationForm extends Component { 13 | 14 | /* Render this step only if order is present and in a valid checkout state. */ 15 | componentWillMount() { 16 | let order = this.props.order; 17 | 18 | if (!CheckoutStepCalculator.isStepEditable(order.checkout_steps, 'confirm', order.state)){ 19 | this.props.handleCheckoutStepNotEditable(order, this.props.placedOrder); 20 | } 21 | }; 22 | 23 | componentDidMount() { 24 | this.props.setCurrentCheckoutStep(); 25 | }; 26 | 27 | handleFormSubmit (formData) { 28 | this.props.handleFormSubmit(formData, this.props.order); 29 | }; 30 | 31 | _shipmentLineItemsMarkup () { 32 | let thisShipment = this.props.order.shipments[0]; 33 | let shipmentLineItems = this.props.order.line_items.filter((lineItem) => { 34 | return (lineItem.variant_id !== thisShipment.manifest.variant_id); 35 | }); 36 | return shipmentLineItems.map((lineItem, idx) => { 37 | return 38 | }); 39 | }; 40 | 41 | 42 | render() { 43 | const { handleSubmit, valid, submitting } = this.props; 44 | return ( 45 | 46 | 49 |
    50 |
    51 |
    52 |

    53 | 57 | : 58 |

    59 |
    60 |
    61 |
    62 |

    63 | 67 | : 68 |

    69 |
    70 |
    71 |
    72 |
    73 |
    74 |
    75 |

    76 | 80 |

    81 | { this._shipmentLineItemsMarkup() } 82 |
    83 |
    84 |
    85 | 93 |
    94 |
    95 |
    96 |
    97 | ); 98 | }; 99 | }; 100 | 101 | ConfirmationForm = reduxForm({ 102 | form: 'confirmationForm' 103 | })(ConfirmationForm); 104 | 105 | export default ConfirmationForm; 106 | -------------------------------------------------------------------------------- /src/actions/checkout.js: -------------------------------------------------------------------------------- 1 | import { push } from 'react-router-redux'; 2 | 3 | import OrdersAPI from '../apis/order'; 4 | import CheckoutAPI from '../apis/checkout'; 5 | import CheckoutStepCalculator from '../services/checkout-step-calculator'; 6 | import InvalidCheckoutStepException from '../errors/invalid-checkout-step'; 7 | import InvalidOrderTransitionException from '../errors/invalid-order-transition'; 8 | import Actions from './'; 9 | import { tokenForAPI } from './utils'; 10 | 11 | import APP_ROUTES from '../constants/app-routes'; 12 | 13 | const checkout = { 14 | goToNextStep: (order, formData = {}) => { 15 | return (dispatch, getState) => { 16 | // Show Loader 17 | // Send next request to API. 18 | // If success -> 19 | // reset the order 20 | // re-route to the next step. 21 | // hide loader 22 | // show success flash 23 | // If failed -> 24 | // Do not touch the order. 25 | // show error message in flash. 26 | // hide loader 27 | 28 | // Edge Cases: 29 | // Return if order is already complete. 30 | 31 | let orderState = order.state; 32 | let checkoutSteps = order.checkout_steps; 33 | 34 | if (CheckoutStepCalculator.isLastStep(checkoutSteps, orderState)) { 35 | dispatch(Actions.showFlash('Your order has already been placed. Thanks!')); 36 | dispatch(push(APP_ROUTES.homePageRoute)); 37 | } 38 | else { 39 | dispatch (Actions.displayLoader()); 40 | let currentStep = getState().currentCheckoutStep; 41 | let tokenParam = tokenForAPI(getState().user.token, order.guest_token); 42 | let apiPromise; 43 | 44 | if (CheckoutStepCalculator.isPristineStep(checkoutSteps, currentStep, orderState)) { 45 | apiPromise = CheckoutAPI.update(order.number, tokenParam, formData); 46 | } 47 | else { 48 | apiPromise = OrdersAPI.update(order.number, tokenParam, formData); 49 | } 50 | 51 | apiPromise.then((response) => { 52 | dispatch(Actions.updateOrderInState(response.body)); 53 | let newOrder = getState().order; 54 | dispatch (push(checkout._fetchNextRoute(newOrder, currentStep))); 55 | dispatch (Actions.hideLoader()); 56 | dispatch (Actions.showFlash(`Successfully saved ${currentStep} form.`)); 57 | }, 58 | (error) => { 59 | dispatch (Actions.hideLoader()); 60 | dispatch (Actions.showFlash(error.response.body.error, 'danger')); 61 | }); 62 | 63 | return apiPromise; 64 | } 65 | }; 66 | }, 67 | 68 | _fetchNextRoute: (order, currentStep) => { 69 | try { 70 | return checkout._nextCheckoutStepRoute(order, currentStep); 71 | } catch (err) { 72 | if (err instanceof InvalidCheckoutStepException) { 73 | return APP_ROUTES.cartPageRoute; 74 | } 75 | else { 76 | if (err instanceof InvalidOrderTransitionException) { 77 | return APP_ROUTES.homePageRoute; 78 | } 79 | } 80 | } 81 | }, 82 | 83 | _nextCheckoutStepRoute: (order, currentStep) => { 84 | let currentStepIndex = order.checkout_steps.indexOf(currentStep.trim()); 85 | 86 | /* If currentStep is a valid step and not the last step. 87 | Last step is 'complete' which means order is placed. */ 88 | if (currentStepIndex > -1 || currentStep === 'cart') { 89 | if (!CheckoutStepCalculator.isLastStep(order.checkout_steps, currentStep)) { 90 | const nextStep = CheckoutStepCalculator.next(order.checkout_steps, currentStep); 91 | 92 | return APP_ROUTES.checkout[`${ nextStep }PageRoute`]; 93 | } 94 | else { 95 | throw new InvalidOrderTransitionException(); 96 | } 97 | } 98 | else { 99 | throw new InvalidCheckoutStepException(); 100 | } 101 | } 102 | 103 | }; 104 | 105 | export default checkout; 106 | -------------------------------------------------------------------------------- /src/components/checkout-steps/payment-form.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Field, reduxForm, formValueSelector, SubmissionError } from 'redux-form'; 3 | import { connect } from 'react-redux'; 4 | import { FormattedMessage } from 'react-intl'; 5 | 6 | import Layout from "../layout"; 7 | import BaseCheckoutLayout from "./base-checkout-layout"; 8 | import CardFields from './payment/card-fields'; 9 | import CheckoutStepCalculator from '../../services/checkout-step-calculator'; 10 | import ErrorMessageFormatter from '../../services/error-message-formatter'; 11 | 12 | class PaymentForm extends Component { 13 | 14 | /* Render this step only if order is present and in a valid checkout state. */ 15 | componentWillMount() { 16 | let order = this.props.order; 17 | 18 | if (!CheckoutStepCalculator.isStepEditable(order.checkout_steps, 'payment', order.state)){ 19 | this.props.handleCheckoutStepNotEditable(order, this.props.placedOrder); 20 | } 21 | }; 22 | 23 | componentDidMount() { 24 | this.props.setCurrentCheckoutStep(); 25 | }; 26 | 27 | handlePaymentFormSubmit (formData) { 28 | return this.props.handlePaymentFormSubmit(formData, this.props.order).then((response) => { 29 | }, 30 | (error) => { 31 | let sanitizedErrors = this.formattedErrorMessages(error.response.body.errors); 32 | this.props.showFormErrors(sanitizedErrors); 33 | 34 | throw new SubmissionError({ order: sanitizedErrors }); 35 | }); 36 | }; 37 | 38 | render() { 39 | const { handleSubmit, valid, submitting, order } = this.props; 40 | let paymentMethods = order.payment_methods || []; 41 | let paymentMethodMarkup = paymentMethods.map((paymentMethod, idx) => { 42 | return ( 43 |
    44 | 52 |
    53 | ) 54 | }); 55 | return ( 56 | 57 | 60 |
    61 | 62 |
    63 | { paymentMethodMarkup } 64 |
    65 | { this.props.useCard==="2" && 66 | 67 | } 68 |
    69 | 77 |
    78 | 79 |
    80 |
    81 | ); 82 | }; 83 | 84 | formattedErrorMessages (errors) { 85 | let formErrors = errors['payments.Credit Card']; 86 | return ErrorMessageFormatter.formatFormSubmissionErrors(formErrors); 87 | }; 88 | }; 89 | 90 | PaymentForm = reduxForm({ 91 | form: 'paymentForm' 92 | })(PaymentForm); 93 | 94 | const selector = formValueSelector('paymentForm'); 95 | PaymentForm = connect( 96 | state => { 97 | const useCard = selector(state, 'order[payments_attributes][0][payment_method_id]'); 98 | return { 99 | useCard 100 | }; 101 | } 102 | )(PaymentForm) 103 | 104 | export default PaymentForm; 105 | -------------------------------------------------------------------------------- /locales/en/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "en": { 3 | "label.itemTotal": "Item Total", 4 | "label.shippingTotal": "Shipping Total", 5 | "label.adjustmentTotal": "Adjustment Total", 6 | "label.orderTotal": "Order Total", 7 | "label.outOfStock": "Out of Stock", 8 | "label.addToCart": "Add To Cart", 9 | "label.goToCart": "Go To Cart", 10 | "label.shippingAddress": "Shipping Address", 11 | "label.shipmentState.shipped": "Shipped", 12 | "label.shipmentState.pending": "Shipment under Review", 13 | "label.shipmentState.ready": "Dispatching soon", 14 | "label.shipmentState.canceled": "Shipment Cancelled", 15 | "label.packages": "Package(s)", 16 | "label.paymentStatus": "Payment Status", 17 | "label.buttons.savePayment": "Save Payment Details", 18 | "label.buttons.saveAddress": "Save Address Details", 19 | "label.buttons.saveDelivery": "Save Delivery Details", 20 | "label.buttons.placeOrder": "Place Order", 21 | "label.buttons.save": "Save", 22 | "label.buttons.emptyCart": "Empty Cart", 23 | "label.buttons.confirmOrder": "Confirm your Order", 24 | "label.billingAddress": "Billing Address", 25 | "label.shippingAddress": "Shipping Address", 26 | "label.phone": "Phone", 27 | 28 | 29 | "com.home-page.heading": "Style Collection", 30 | "com.addressForm.genaralInfo": "General Info", 31 | "com.addressForm.billingInfo": "Billing Info", 32 | "com.addressForm.shippingInfo": "Shipping Info", 33 | "com.shipmentForm.subheading": "Please select a shipping method for these Items.", 34 | "com.order.summaryHeader": "Order Summary", 35 | "com.header.menu.myOrders": "My Orders", 36 | "com.confirmationForm.orderItems": "Order Items", 37 | "com.checkoutSuccessPage.successMessage": "Your Order has been placed successfully!", 38 | "com.order--list.yourOrders": "Your Orders", 39 | "com.cart--show.header": "Shopping Cart", 40 | "com.cart--show.cartEmptyHeading": "Your cart is empty. Add some items to proceed.", 41 | "com.cart--show.helpMessage": "Need Help? Call: 999-8975-0354", 42 | "com.cart--show.continueShopping": "Continue Shopping", 43 | "com.cart--show.tableHeading.product": "Product", 44 | "com.cart--show.tableHeading.price": "Price", 45 | "com.cart--show.tableHeading.qty": "Qty", 46 | "com.cart--show.tableHeading.total": "Total", 47 | "com.cart--show.tableHeading.actions": "Actions", 48 | 49 | 50 | "field.addressForm.email": "Email", 51 | "field.addressForm.firstName": "First Name", 52 | "field.addressForm.lastName": "Last Name", 53 | "field.addressForm.address1": "Address Line 1", 54 | "field.addressForm.address2": "Address Line 2", 55 | "field.addressForm.city": "City", 56 | "field.addressForm.country": "Country", 57 | "field.addressForm.state": "State", 58 | "field.addressForm.zipCode": "Zip Code", 59 | "field.addressForm.phone": "Phone", 60 | "field.addressForm.useBilling": "Ship to Billing Address", 61 | "field.addressForm.rememberAddress": "Remember this Address", 62 | "field.paymentForm.nameOnCard": "Name on card", 63 | "field.paymentForm.cardNumber": "Card Number", 64 | "field.paymentForm.cardExpiry": "Card expiry", 65 | "field.paymentForm.verificationValue": "Verification value", 66 | 67 | 68 | "shared.email": "Email", 69 | "shared.password": "Password", 70 | "shared.confirmPassword": "Confirm Password", 71 | "shared.login": "Login", 72 | "shared.signUp": "Sign Up", 73 | "shared.signOut": "Sign Out", 74 | "shared.loading": "Fetching Data ...", 75 | "shared.models.variant": "Variant", 76 | "shared.models.productProperties": "Product Properties", 77 | "shared.models.shippingMethod": "Shipping Method", 78 | "shared.models.shipment": "Packages", 79 | "shared.models.country": "Country", 80 | "shared.models.state": "State", 81 | "shared.attributes.description": "Description", 82 | "shared.attributes.shippingCharges": "Shipping Charges", 83 | "shared.attributes.shipmentNumber": "Ref", 84 | "shared.attributes.orderNumber": "Ref" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/styles/pages/product-show.scss: -------------------------------------------------------------------------------- 1 | $imageBlockHeight: 500px; 2 | 3 | .product-details-section { 4 | padding-top: 50px; 5 | padding-bottom: 50px; 6 | } 7 | 8 | .product-image-block { 9 | .product-img-block { 10 | width: $full; 11 | height: $imageBlockHeight; 12 | display: table; 13 | position: relative; 14 | background: $light-grey-bg; 15 | } 16 | 17 | .product-image-holder { 18 | width: $full; 19 | height: $imageBlockHeight; 20 | display: table-cell; 21 | text-align: center; 22 | vertical-align: middle; 23 | } 24 | 25 | .product-image { 26 | max-width: $full; 27 | max-height: $full; 28 | } 29 | 30 | .product-thumbnails-row { 31 | margin-top: 20px; 32 | } 33 | 34 | .product-image-thumbnail { 35 | width: 92px; 36 | height: 62px; 37 | margin-bottom: 0; 38 | margin-left: 20px; 39 | border: solid 1px $border-grey; 40 | padding: 1px; 41 | display: inline-block; 42 | @include border-radius(0); 43 | 44 | &.selected { 45 | border-color: $color-black; 46 | } 47 | 48 | &:first-child { 49 | margin-left: 0; 50 | } 51 | } 52 | 53 | .product-thumbnail-holder { 54 | width: 90px; 55 | height: 60px; 56 | display: table-cell; 57 | text-align: center; 58 | vertical-align: middle; 59 | } 60 | } 61 | 62 | .product-options-block { 63 | .product-option-row { 64 | margin-top: 20px; 65 | 66 | &:first-child { 67 | margin-top: 0; 68 | } 69 | } 70 | 71 | .product-name { 72 | font-size: 24px; 73 | font-weight: 300; 74 | } 75 | 76 | .product-price { 77 | font-size: 18px; 78 | font-weight: 700; 79 | } 80 | 81 | .variant-block { 82 | width: $full; 83 | padding: 20px 0; 84 | display: table; 85 | 86 | label { 87 | font-size: 11px; 88 | } 89 | 90 | .varient-title { 91 | margin-bottom: 5px; 92 | font-weight: 500; 93 | } 94 | } 95 | 96 | .product-variant-title { 97 | margin-bottom: 20px; 98 | font-size: 18px; 99 | } 100 | 101 | .variant-button { 102 | &.btn-primary { 103 | min-width: 300px; 104 | border-color: $color-black; 105 | color: $color-black; 106 | background: $color-white; 107 | @include border-radius(0); 108 | @include text-shadow(0, 0, 0, transparent); 109 | } 110 | } 111 | 112 | .variant-dropdown-button { 113 | &.btn-primary { 114 | border-color: $color-black; 115 | color: $color-black; 116 | background: $color-white; 117 | @include border-radius(0); 118 | @include text-shadow(0, 0, 0, transparent); 119 | } 120 | } 121 | 122 | .variant-options-dropdown { 123 | &.dropdown-menu { 124 | min-width: $full; 125 | max-height: 200px; 126 | border-color: $color-black; 127 | overflow-x: hidden; 128 | overflow-y: auto; 129 | @include border-radius(0); 130 | 131 | li { 132 | padding: 5px 0; 133 | } 134 | } 135 | } 136 | 137 | .button-primary { 138 | width: $full; 139 | 140 | &.btn { 141 | @include border-radius(0); 142 | } 143 | 144 | .cart-icon { 145 | margin-left: 10px; 146 | } 147 | } 148 | 149 | .product-description-block { 150 | margin-top: 40px; 151 | border: solid 1px $border-grey; 152 | padding: 30px; 153 | background: $light-grey-bg; 154 | 155 | .product-description-content { 156 | margin-top: 10px; 157 | } 158 | } 159 | } 160 | 161 | .product-desription-section { 162 | padding-top: 20px; 163 | 164 | .product-description-content { 165 | margin-top: 10px; 166 | } 167 | 168 | .product-properties-block, 169 | .product-description-block { 170 | margin-top: 40px; 171 | border: solid 1px $border-grey; 172 | padding: 30px; 173 | background: $light-grey-bg; 174 | } 175 | 176 | .product-properties-row { 177 | margin: 0; 178 | padding: 10px 0; 179 | border-top: solid 1px $border-grey; 180 | 181 | &:first-child { 182 | border-top: none; 183 | } 184 | } 185 | 186 | .product-properties-label { 187 | padding-left: 0; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /locales/es/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "es": { 3 | "label.itemTotal": "Total de artículo", 4 | "label.shippingTotal": "Total de gastos de envío", 5 | "label.adjustmentTotal": "Total de ajuste", 6 | "label.orderTotal": "Total del pedido", 7 | "label.outOfStock": "Fuera de stock", 8 | "label.addToCart": "Añadir a la cesta", 9 | "label.goToCart": "Ir a la cesta", 10 | "label.shippingAddress": "Dirección de envío", 11 | "label.shipmentState.shipped": "Enviado", 12 | "label.shipmentState.pending": "Envío bajo revisión", 13 | "label.shipmentState.ready": "Pronto despacho", 14 | "label.shipmentState.canceled": "Envío cancelado", 15 | "label.packages": "Paquete(s)", 16 | "label.paymentStatus": "El estado de pago", 17 | "label.buttons.savePayment": "Guardar detalles de pago", 18 | "label.buttons.saveAddress": "Guardar detalles de la dirección", 19 | "label.buttons.saveDelivery": "Guardar detalles de entrega", 20 | "label.buttons.placeOrder": "Realizar pedido", 21 | "label.buttons.save": "Guardar", 22 | "label.buttons.emptyCart": "Cesta vacía", 23 | "label.buttons.confirmOrder": "Confirme su pedido", 24 | "label.billingAddress": "Dirección de facturación", 25 | "label.shippingAddress": "Dirección de envío", 26 | "label.phone": "Teléfono", 27 | 28 | 29 | "com.home-page.heading": "Colección de estilo", 30 | "com.addressForm.genaralInfo": "Información general", 31 | "com.addressForm.billingInfo": "Información de facturación", 32 | "com.addressForm.shippingInfo": "Información de envío", 33 | "com.shipmentForm.subheading": "Seleccione un método de envío para estos elementos.", 34 | "com.order.summaryHeader": "Resumen de pedido", 35 | "com.header.menu.myOrders": "Mis pedidos", 36 | "com.confirmationForm.orderItems": "Orden de elementos", 37 | "com.checkoutSuccessPage.successMessage": "Su pedido se ha realizado correctamente.", 38 | "com.order--list.yourOrders": "Sus pedidos.", 39 | "com.cart--show.header": "Compras", 40 | "com.cart--show.cartEmptyHeading": "Su carro está vacío. Agregue algunos elementos para proceder.", 41 | "com.cart--show.helpMessage": "¿Necesita ayuda? Llamada: 999-8975-0354", 42 | "com.cart--show.continueShopping": "Continuar con la compra", 43 | "com.cart--show.tableHeading.product": "Producto", 44 | "com.cart--show.tableHeading.price": "Precio", 45 | "com.cart--show.tableHeading.qty": "Cantidad", 46 | "com.cart--show.tableHeading.total": "Total", 47 | "com.cart--show.tableHeading.actions": "Comportamiento", 48 | 49 | 50 | "field.addressForm.email": "Email", 51 | "field.addressForm.firstName": "Nombre de pila", 52 | "field.addressForm.lastName": "Apellido", 53 | "field.addressForm.address1": "Dirección Línea 1", 54 | "field.addressForm.address2": "Dirección Línea 2", 55 | "field.addressForm.city": "Ciudad", 56 | "field.addressForm.country": "País", 57 | "field.addressForm.state": "estado", 58 | "field.addressForm.zipCode": "código postal", 59 | "field.addressForm.phone": "Teléfono", 60 | "field.addressForm.useBilling": "Enviar a la dirección de cobro", 61 | "field.addressForm.rememberAddress": "Recuerde esta dirección", 62 | "field.paymentForm.nameOnCard": "Nombre en la tarjeta", 63 | "field.paymentForm.cardNumber": "Número de tarjeta", 64 | "field.paymentForm.cardExpiry": "Caducidad de tarjeta", 65 | "field.paymentForm.verificationValue": "Valor de verificación", 66 | 67 | 68 | "shared.email": "Email", 69 | "shared.password": "Contraseña", 70 | "shared.confirmPassword": "Confirmar contraseña", 71 | "shared.login": "Iniciar sesión", 72 | "shared.signUp": "Regístrate", 73 | "shared.signOut": "Desconectar", 74 | "shared.loading": "Recuperacion de datos ...", 75 | "shared.models.variant": "Variante", 76 | "shared.models.productProperties": "Propiedades del producto", 77 | "shared.models.shippingMethod": "Método de envío", 78 | "shared.models.shipment": "Paquetes", 79 | "shared.models.country": "País", 80 | "shared.models.state": "estado", 81 | "shared.attributes.description": "Descripción", 82 | "shared.attributes.shippingCharges": "Gastos de envío", 83 | "shared.attributes.shipmentNumber": "Árbitro", 84 | "shared.attributes.orderNumber": "Árbitro" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/apis/ams-adapters/spree-api-order-adapter.js: -------------------------------------------------------------------------------- 1 | const SpreeAPIOrderAdapter = { 2 | processList: (orderListAMS) => { 3 | orderListAMS.orders.forEach((order) => { 4 | SpreeAPIOrderAdapter._process(order, orderListAMS); 5 | }); 6 | 7 | return orderListAMS; 8 | }, 9 | 10 | processItem: (orderListAMS) => { 11 | SpreeAPIOrderAdapter._process(orderListAMS.order, orderListAMS); 12 | 13 | return orderListAMS.order; 14 | }, 15 | 16 | /* 17 | PRIVATE METHODS 18 | */ 19 | _process: (order, orderListAMS) => { 20 | order.bill_address = SpreeAPIOrderAdapter._buildAddress(order.bill_address_id, orderListAMS); 21 | order.ship_address = SpreeAPIOrderAdapter._buildAddress(order.ship_address_id, orderListAMS); 22 | order.line_items = SpreeAPIOrderAdapter._buildLineItems(order.line_item_ids, orderListAMS); 23 | order.shipments = SpreeAPIOrderAdapter._buildShipments(order.shipment_ids, orderListAMS); 24 | order.payments = SpreeAPIOrderAdapter._buildPayments(order.payment_ids, orderListAMS); 25 | order.countries = orderListAMS.countries; 26 | order.states = orderListAMS.states; 27 | order.payment_methods = orderListAMS.payment_methods; 28 | }, 29 | 30 | _getItem: (itemId, itemCollection) => { 31 | return itemCollection.find((item) => { 32 | return item.id === itemId; 33 | }); 34 | }, 35 | 36 | _buildAddress: (addressId, orderListAMS) => { 37 | let address = SpreeAPIOrderAdapter._getItem(addressId, orderListAMS.addresses); 38 | if (address) { 39 | address.country = SpreeAPIOrderAdapter._getItem(address.country_id, orderListAMS.countries); 40 | address.state = SpreeAPIOrderAdapter._getItem(address.state_id, orderListAMS.states); 41 | } 42 | return address; 43 | }, 44 | 45 | _buildLineItems: (lineItemIds, orderListAMS) => { 46 | let lineItems = []; 47 | lineItemIds.forEach((lineItemId) => { 48 | let thisLineItem = SpreeAPIOrderAdapter._getItem(lineItemId, orderListAMS.line_items); 49 | 50 | if (thisLineItem) { 51 | thisLineItem.variant = SpreeAPIOrderAdapter._getItem(thisLineItem.variant_id, orderListAMS.variants); 52 | if (thisLineItem.variant) { 53 | thisLineItem.variant.images = SpreeAPIOrderAdapter._buildVariantImages(thisLineItem.variant.image_ids, orderListAMS); 54 | } 55 | 56 | lineItems.push(thisLineItem); 57 | } 58 | }); 59 | 60 | return lineItems; 61 | }, 62 | 63 | _buildVariantImages: (imageIds, orderListAMS) => { 64 | let images = []; 65 | imageIds.forEach((imageId) => { 66 | let thisImage = SpreeAPIOrderAdapter._getItem(imageId, orderListAMS.images); 67 | images.push(thisImage); 68 | }); 69 | 70 | return images; 71 | }, 72 | 73 | _buildShipments: (shipmentIds, orderListAMS) => { 74 | let shipments = []; 75 | shipmentIds.forEach((shipmentId) => { 76 | let thisShipment = SpreeAPIOrderAdapter._getItem(shipmentId, orderListAMS.shipments); 77 | thisShipment.selected_shipping_rate = SpreeAPIOrderAdapter._getItem(thisShipment.selected_shipping_rate_id, orderListAMS.shipping_rates); 78 | thisShipment.shipping_rates = SpreeAPIOrderAdapter._buildShippingRates(thisShipment.shipping_rate_ids, orderListAMS); 79 | thisShipment.manifest = SpreeAPIOrderAdapter._buildShipmentManifest(thisShipment.line_item_ids, orderListAMS); 80 | shipments.push(thisShipment); 81 | }); 82 | 83 | return shipments; 84 | }, 85 | 86 | _buildPayments: (paymentIds, orderListAMS) => { 87 | let payments = []; 88 | paymentIds.forEach((paymentId) => { 89 | payments.push (SpreeAPIOrderAdapter._getItem(paymentId, orderListAMS.payments)); 90 | }); 91 | 92 | return payments; 93 | }, 94 | 95 | _buildShippingRates: (shippingRateIds, orderListAMS) => { 96 | let shippingRates = []; 97 | shippingRateIds.forEach((shippingRateId) => { 98 | let thisShippingRate = SpreeAPIOrderAdapter._getItem(shippingRateId, orderListAMS.shipping_rates); 99 | shippingRates.push(thisShippingRate); 100 | }); 101 | 102 | return shippingRates; 103 | }, 104 | 105 | _buildShipmentManifest: (lineItemIds, orderListAMS) => { 106 | let manifest = []; 107 | lineItemIds.forEach((lineItemId) => { 108 | let thisLineItem = SpreeAPIOrderAdapter._getItem(lineItemId, orderListAMS.line_items); 109 | manifest.push({ variant_id: thisLineItem.variant_id, quantity: thisLineItem.quantity }); 110 | }); 111 | 112 | return manifest; 113 | } 114 | 115 | }; 116 | 117 | export default SpreeAPIOrderAdapter; 118 | -------------------------------------------------------------------------------- /src/components/order/shipment.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import Address from './address'; 5 | import LineItem from './line-item'; 6 | 7 | class Shipment extends Component { 8 | render() { 9 | let thisShipment = this.props.shipment; 10 | 11 | return ( 12 |
    13 |
    14 |
    15 |
    16 |
    17 | { this._shipmentStateMarkup() } 18 |
    19 |
    20 | 27 | 34 | 41 |
    42 |
    43 | 44 |
    45 | { this._shipmentLineItemsMarkup() } 46 |
    47 |
    48 | 49 |
    50 |
    51 | 58 |
    59 |
    60 |
    61 |
    62 | 63 |
    64 |
    65 | 66 | 70 | : 71 | ${ this.props.order.total } 72 |
    73 |
    74 |
    75 | ); 76 | }; 77 | 78 | _isShipped() { 79 | return( this.props.shipment.state === "shipped" ); 80 | }; 81 | 82 | _shipmentStateMarkup() { 83 | let thisShipment = this.props.shipment; 84 | 85 | if (this._isShipped()) { 86 | return ( 87 |
    88 | 92 | { thisShipment.shipped_at } 93 |
    94 | ); 95 | } 96 | else { 97 | if (thisShipment.state === "pending") { 98 | return ( 99 | 103 | ); 104 | } 105 | else if (thisShipment.state === "ready") { 106 | return ( 107 | 111 | ); 112 | } 113 | 114 | else if (thisShipment.state === "canceled") { 115 | return ( 116 | 120 | ); 121 | } 122 | 123 | } 124 | }; 125 | 126 | _shipmentLineItemsMarkup() { 127 | let thisShipment = this.props.shipment; 128 | 129 | let shipmentLineItems = this.props.orderLineItems.filter((lineItem) => { 130 | return thisShipment.line_item_ids.indexOf(lineItem.id) !== -1; 131 | }); 132 | 133 | return shipmentLineItems.map((lineItem, idx) => { 134 | return 135 | }); 136 | }; 137 | }; 138 | 139 | export default Shipment; 140 | -------------------------------------------------------------------------------- /src/components/user-signup.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { reduxForm, Field } from 'redux-form'; 4 | 5 | import Modal from './shared/modal'; 6 | import FlashConnector from '../containers/flash-connector'; 7 | 8 | class userSignup extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.closeModal = this.closeModal.bind(this); 13 | this.handleFormSubmit = this.handleFormSubmit.bind(this); 14 | } 15 | 16 | closeModal() { 17 | this.props.closeModal(); 18 | } 19 | 20 | handleFormSubmit(formData) { 21 | this.props.submitSignupForm(formData).then((response) => { 22 | this.closeModal(); 23 | }); 24 | } 25 | 26 | render() { 27 | const { handleSubmit } = this.props; 28 | return ( 29 | 30 |
    31 |
    32 |
    33 |

    SignUp

    34 | 35 |
    36 | 37 |
    38 | 39 |
    40 |
    41 | 47 |
    48 | 53 |
    54 |
    55 |
    56 | 57 |
    58 |
    59 |
    60 | 61 | 67 |
    68 | 73 |
    74 |
    75 |
    76 | 82 |
    83 | 88 |
    89 |
    90 |
    91 |
    92 |
    93 |
    94 |
    95 | 101 |
    102 |
    103 |
    104 |
    105 |
    106 |
    107 |
    108 |
    109 | ); 110 | } 111 | } 112 | 113 | userSignup = reduxForm({ 114 | form: 'userSignup' 115 | })(userSignup); 116 | export default userSignup; 117 | -------------------------------------------------------------------------------- /src/components/styles/theme-global.scss: -------------------------------------------------------------------------------- 1 | @import 'core/fonts.scss'; 2 | @import 'core/variables.scss'; 3 | @import 'core/mixins.scss'; 4 | @import 'core/modal.scss'; 5 | 6 | @import 'components/mobile-nav.scss'; 7 | @import 'components/footer.scss'; 8 | 9 | @import 'pages/product-show.scss'; 10 | @import 'pages/cart.scss'; 11 | @import 'pages/checkout.scss'; 12 | @import 'pages/orders.scss'; 13 | 14 | html { 15 | height: $full; 16 | } 17 | 18 | body { 19 | min-height: $full; 20 | padding-bottom: 80px; 21 | position: relative; 22 | font-family: $roboto; 23 | background: $color-white; 24 | } 25 | 26 | h1, 27 | h2, 28 | h3, 29 | h4, 30 | h5, 31 | h6, 32 | ul, 33 | li, 34 | dl, 35 | dt, 36 | dd, 37 | p { 38 | margin: 0; 39 | padding: 0; 40 | list-style: none; 41 | } 42 | 43 | h1, 44 | h2, 45 | h3, 46 | h4 { 47 | font-family: $oswald; 48 | } 49 | 50 | a, 51 | button, 52 | input { 53 | outline: none; 54 | 55 | &:focus { 56 | outline: none; 57 | } 58 | } 59 | 60 | a { 61 | color: $color-black; 62 | &:focus { 63 | outline: none; 64 | text-decoration: none; 65 | } 66 | 67 | &:hover { 68 | color: $link-color; 69 | text-decoration: none; 70 | } 71 | } 72 | 73 | .container, 74 | .container-fluid { 75 | &.no-margin { 76 | margin: 0; 77 | padding: 0; 78 | } 79 | } 80 | 81 | .body-container { 82 | padding-top: 60px; 83 | padding-bottom: 60px; 84 | } 85 | 86 | .section-heading { 87 | border-bottom: solid 1px $border-grey; 88 | padding-bottom: 10px; 89 | font-size: 24px; 90 | } 91 | 92 | .product-section { 93 | margin-top: 30px; 94 | } 95 | 96 | .button-primary { 97 | border: solid 1px $color-black; 98 | padding: 12px 40px; 99 | display: inline-block; 100 | color: $color-white; 101 | background: $color-black; 102 | font-family: $oswald; 103 | font-size: 14px; 104 | font-weight: normal; 105 | text-transform: uppercase; 106 | letter-spacing: 1px; 107 | cursor: pointer; 108 | @include transition(all, 0.3s, ease-in-out); 109 | 110 | &.button-small { 111 | height: 30px; 112 | padding: 0 10px; 113 | font-size: 11px; 114 | line-height: 30px; 115 | } 116 | 117 | &.button-green { 118 | border-color: $link-color; 119 | background: $link-color; 120 | 121 | &:hover { 122 | border-color: $color-black; 123 | color: $color-white; 124 | background: $color-black 125 | } 126 | } 127 | 128 | &.button-white { 129 | color: $color-black; 130 | background: $color-white; 131 | 132 | &:hover { 133 | color: $color-white; 134 | background: $color-black; 135 | } 136 | } 137 | 138 | &.button-red { 139 | width: 40px; 140 | height: 36px; 141 | border-color: $color-error-red; 142 | padding: 0; 143 | line-height: 36px; 144 | text-align: center; 145 | background: $color-error-red; 146 | 147 | &:hover { 148 | border-color: $color-black; 149 | color: $color-white; 150 | background: $color-black; 151 | } 152 | } 153 | 154 | &:hover { 155 | color: $color-black; 156 | background: $color-white; 157 | } 158 | } 159 | 160 | .primary-input-field { 161 | width: $full; 162 | height: 40px; 163 | border: solid 1px $border-grey; 164 | padding: 0 10px; 165 | background: $color-white; 166 | } 167 | 168 | .infinite-loader { 169 | width: $full; 170 | height: 40px; 171 | position: fixed; 172 | bottom: 0; 173 | left: 0; 174 | z-index: 99; 175 | color: $color-white; 176 | text-align: center; 177 | line-height: 40px; 178 | background: $link-color; 179 | 180 | .glyphicon { 181 | padding: 0 10px; 182 | } 183 | } 184 | 185 | .rotate-animation{ 186 | @include animation(spin, 2000ms, infinite, linear); 187 | } 188 | 189 | @-ms-keyframes spin { 190 | from { -ms-transform: rotate(0deg); } 191 | to { -ms-transform: rotate(360deg); } 192 | } 193 | @-moz-keyframes spin { 194 | from { -moz-transform: rotate(0deg); } 195 | to { -moz-transform: rotate(360deg); } 196 | } 197 | @-webkit-keyframes spin { 198 | from { -webkit-transform: rotate(0deg); } 199 | to { -webkit-transform: rotate(360deg); } 200 | } 201 | @keyframes spin { 202 | from { 203 | transform:rotate(0deg); 204 | } 205 | to { 206 | transform:rotate(360deg); 207 | } 208 | } 209 | 210 | @media (max-width: 767px) { 211 | .body-container { 212 | padding-top: 30px; 213 | padding-bottom: 30px; 214 | } 215 | 216 | .section-heading { 217 | font-size: 18px; 218 | } 219 | 220 | .homepage-slider { 221 | .carousel-control { 222 | display: none; 223 | } 224 | } 225 | } 226 | 227 | @media (min-width: 768px) and (max-width: 1023px) { 228 | .homepage-slider { 229 | .carousel-control { 230 | width: 8% 231 | } 232 | } 233 | } 234 | --------------------------------------------------------------------------------