├── .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 |
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 |
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 |
20 | { errorMessageList }
21 |
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 |
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 | //
9 | //
10 | // ashwani
11 | //
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 | 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 |
{ label }
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 |
31 |
35 | :
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 |
20 | { country.name }
21 |
22 | );
23 | });
24 |
25 | return (
26 |
30 | { countryOptionsMarkup }
31 |
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 |
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 |
25 | { this.props.currentVariant.options_text }
26 |
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 |
37 |
41 | : { thisOrder.number }
42 | { thisOrder.shipments.length }
43 |
44 |
48 |
49 |
50 |
54 | : ${ thisOrder.total }
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 |
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 |
53 | { this._addToCartMarkup() }
54 |
55 |
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 |
51 |
55 | { shippingRate.name }
56 | ( { shippingRate.display_cost } )
57 |
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 |
16 | { property[0] }
17 |
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 |
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 |
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 |
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 |
45 |
49 |
50 | { paymentMethod.name }
51 |
52 |
53 | )
54 | });
55 | return (
56 |
57 |
60 |
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 |
21 |
25 | : { thisShipment.selected_shipping_rate.name }
26 |
27 |
28 |
32 | : { thisShipment.selected_shipping_rate.display_cost }
33 |
34 |
35 |
39 | : { thisShipment.number }
40 |
41 |
42 |
43 |
44 |
45 | { this._shipmentLineItemsMarkup() }
46 |
47 |
48 |
49 |
50 |
51 |
52 |
56 | :
57 |
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 |
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 |
--------------------------------------------------------------------------------