diff --git a/.babelrc b/.babelrc index 1e0e1722..2b94f11e 100644 --- a/.babelrc +++ b/.babelrc @@ -21,7 +21,12 @@ "transform-es2015-sticky-regex", "transform-es2015-template-literals", "transform-es2015-typeof-symbol", - "transform-es2015-unicode-regex" + "transform-es2015-unicode-regex", + ["typecheck", { + "disable": { + "production": true + } + }] ], "presets": ["stage-0", "react"], "env": { diff --git a/.eslintrc b/.eslintrc index 05de1f8d..cad7691b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,7 +23,8 @@ "ignorePattern": "^\\s*var\\s.+=\\s*require\\s*\\(" } ], - "arrow-body-style": 0 + "arrow-body-style": 0, + "react/prop-types": 0 }, "parser": "babel-eslint", } diff --git a/package.json b/package.json index 3e38d647..f49d5712 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "babel-plugin-transform-class-properties": "^6.5.2", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.5.0", + "babel-plugin-typecheck": "^3.7.0", "babel-polyfill": "^6.5.0", "babel-preset-es2015": "^6.5.0", "babel-preset-react": "^6.5.0", diff --git a/src/components/categories/categories.jsx b/src/components/categories/categories.jsx index 5ea63a34..82ad17b2 100644 --- a/src/components/categories/categories.jsx +++ b/src/components/categories/categories.jsx @@ -5,6 +5,7 @@ import _ from 'lodash'; import styles from './categories.css'; import cssModules from 'react-css-modules'; import { connect } from 'react-redux'; +import type { HTMLElement } from '../../types'; import * as actions from '../../modules/categories'; @@ -21,7 +22,7 @@ class Categories extends React.Component { this.props.fetchCategories(); } - render(): Element { + render(): HTMLElement { const categoryItems = _.map(this.props.list, (item) => { const key = `category-${item.replace(/\s/g, '-')}`; return ( diff --git a/src/components/common/icon.jsx b/src/components/common/icon.jsx index 3836e0a7..6dba6f80 100644 --- a/src/components/common/icon.jsx +++ b/src/components/common/icon.jsx @@ -1,9 +1,10 @@ /* @flow */ import classNames from 'classnames'; -import React, { PropTypes, Element } from 'react'; +import React from 'react'; +import type { HTMLElement } from '../../types'; -function wrapSpinner(svg: Element, className: string) { +function wrapSpinner(svg: HTMLElement, className: string) { if (className.indexOf('spinner') > -1) { return (
{svg}
@@ -13,7 +14,15 @@ function wrapSpinner(svg: Element, className: string) { return svg; } -const Icon = (props: Object) => { +type IconSize = 'm' | 'l' | 'xl' | 'xxl'; + +type IconProps = { + size: ?IconSize; + name: string, + className: ?string, +} + +const Icon = (props: IconProps) => { const name = `#${props.name}-icon`; const useTag = ``; @@ -36,10 +45,4 @@ const Icon = (props: Object) => { ); }; -Icon.propTypes = { - size: PropTypes.oneOf(['m', 'l', 'xl', 'xxl']), - name: PropTypes.string.isRequired, - className: PropTypes.string, -}; - export default Icon; diff --git a/src/components/editable-block/index.css b/src/components/editable-block/index.css new file mode 100644 index 00000000..aa328da8 --- /dev/null +++ b/src/components/editable-block/index.css @@ -0,0 +1,24 @@ +@import "colors.css"; + +.editable-block { + padding: 20px; + background: var(--color-panels-bg); +} + +.header { + margin-top: 5px; + display: flex; +} + +.title { + font-size: 22px; + line-height: 27px; + flex-grow: 1; +} + +.edit { + color: #9E9E9E; + text-align: right; +} + +.content {} diff --git a/src/components/editable-block/index.jsx b/src/components/editable-block/index.jsx new file mode 100644 index 00000000..d17e4602 --- /dev/null +++ b/src/components/editable-block/index.jsx @@ -0,0 +1,37 @@ + +/* @flow */ + +import React from 'react'; +import cssModules from 'react-css-modules'; +import styles from './index.css'; + +import type { HTMLElement } from '../../types'; + +type EditableProps = { + isEditing: boolean; + viewContent: ?HTMLElement|string; + editContent: ?HTMLElement|string; + title: string; +}; + +const EditableBlock = (props: EditableProps) => { + const content = props.isEditing ? props.editContent : props.viewContent; + + const editLink = props.isEditing ?
EDIT
: null; + + return ( +
+
+
{props.title}
+ {editLink} +
+
{content}
+
+ ); +}; + +EditableBlock.defaulProps = { + isEditing: false, +}; + +export default cssModules(EditableBlock, styles); diff --git a/src/components/forms/formfield.css b/src/components/forms/formfield.css index ce5d11c1..b58b50aa 100644 --- a/src/components/forms/formfield.css +++ b/src/components/forms/formfield.css @@ -4,11 +4,13 @@ .has-error { & > input, & > textarea { - border-color: var(--redish); + border-color: var(--color-error); } } .error { color: var(--redish); text-align: left; + font-size: 13px; + line-height: 16px; } diff --git a/src/components/pages/auth/auth.css b/src/components/pages/auth/auth.css index bfaf3b75..b0c61d52 100644 --- a/src/components/pages/auth/auth.css +++ b/src/components/pages/auth/auth.css @@ -10,7 +10,7 @@ lost-offset: -4/11; } -.icon { +.logo { width: 96px; height: 96px; margin-bottom: 48px; @@ -86,7 +86,7 @@ margin-bottom: 20px; } - .icon { + .logo { width: 48px; height: 48px; margin-bottom: 20px; diff --git a/src/components/pages/auth/auth.jsx b/src/components/pages/auth/auth.jsx index 7ab9a09c..47b657c0 100644 --- a/src/components/pages/auth/auth.jsx +++ b/src/components/pages/auth/auth.jsx @@ -9,7 +9,7 @@ import Icon from '../../common/icon'; const Auth = props => { return (
- + {props.children}
); diff --git a/src/components/pages/auth/login.jsx b/src/components/pages/auth/login.jsx index 4a00047d..cf02bd38 100644 --- a/src/components/pages/auth/login.jsx +++ b/src/components/pages/auth/login.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, Element } from 'react'; +import React, { Component } from 'react'; import cssModules from 'react-css-modules'; import styles from './auth.css'; import { autobind } from 'core-decorators'; @@ -11,6 +11,8 @@ import Button from '../../common/buttons'; import WrapToLines from '../../common/wrap-to-lines'; import { Link } from 'react-router'; +import type { HTMLElement } from '../../../types'; + type AuthState = { email: string, @@ -41,7 +43,7 @@ export default class Auth extends Component { }); } - render(): Element { + render(): HTMLElement { const { password, email } = this.state; return ( diff --git a/src/components/pages/auth/reset-password.jsx b/src/components/pages/auth/reset-password.jsx index b9c7b811..396d93ec 100644 --- a/src/components/pages/auth/reset-password.jsx +++ b/src/components/pages/auth/reset-password.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, Element, PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import cssModules from 'react-css-modules'; import styles from './auth.css'; import { autobind } from 'core-decorators'; @@ -12,6 +12,8 @@ import { TextInput } from '../../common/inputs'; import { FormField } from '../../forms'; import Button from '../../common/buttons'; +import type { HTMLElement } from '../../../types'; + type ResetState = { isReseted: boolean; }; @@ -74,7 +76,7 @@ export default class RestorePassword extends Component { return Promise.resolve({_error: null}); } - get topMessage(): Element { + get topMessage(): HTMLElement { const { isReseted } = this.state; const { error } = this.props; @@ -101,7 +103,7 @@ export default class RestorePassword extends Component { ); } - get passwordFields(): ?Array { + get passwordFields(): ?HTMLElement[] { const { isReseted } = this.state; const { fields: {passwd1, passwd2}} = this.props; @@ -122,7 +124,7 @@ export default class RestorePassword extends Component { this.props.dispatch(routeActions.push('/login')); } - get primaryButton(): Element { + get primaryButton(): HTMLElement { const { isReseted } = this.state; if (isReseted) { @@ -134,7 +136,7 @@ export default class RestorePassword extends Component { return ; } - render(): Element { + render(): HTMLElement { const { handleSubmit } = this.props; return ( diff --git a/src/components/pages/auth/restore-password.jsx b/src/components/pages/auth/restore-password.jsx index d4bf4247..bda38424 100644 --- a/src/components/pages/auth/restore-password.jsx +++ b/src/components/pages/auth/restore-password.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, Element, PropTypes } from 'react'; +import React, { Component, PropTypes } from 'react'; import cssModules from 'react-css-modules'; import styles from './auth.css'; import { autobind } from 'core-decorators'; @@ -13,6 +13,8 @@ import { FormField } from '../../forms'; import Button from '../../common/buttons'; import { Link } from 'react-router'; +import type { HTMLElement } from '../../../types'; + type FormData = { email: string; }; @@ -59,7 +61,7 @@ export default class RestorePassword extends Component { }); } - get topMessage(): Element { + get topMessage(): HTMLElement { const { emailSent } = this.state; const { fields: {email}, error } = this.props; @@ -86,7 +88,7 @@ export default class RestorePassword extends Component { ); } - get emailField(): ?Element { + get emailField(): ?HTMLElement { const { emailSent } = this.state; const { fields: {email}} = this.props; @@ -104,7 +106,7 @@ export default class RestorePassword extends Component { this.props.dispatch(routeActions.push('/login')); } - get primaryButton(): Element { + get primaryButton(): HTMLElement { const { emailSent } = this.state; if (emailSent) { @@ -116,7 +118,7 @@ export default class RestorePassword extends Component { return ; } - get switchStage(): Element { + get switchStage(): ?HTMLElement { const { emailSent } = this.state; if (!emailSent) { @@ -128,7 +130,7 @@ export default class RestorePassword extends Component { } } - render(): Element { + render(): HTMLElement { const {handleSubmit} = this.props; return ( diff --git a/src/components/pages/auth/signup.jsx b/src/components/pages/auth/signup.jsx index 18f6c276..65e58dbf 100644 --- a/src/components/pages/auth/signup.jsx +++ b/src/components/pages/auth/signup.jsx @@ -1,6 +1,6 @@ /* @flow */ -import React, { Component, Element } from 'react'; +import React, { Component } from 'react'; import cssModules from 'react-css-modules'; import styles from './auth.css'; import { autobind } from 'core-decorators'; @@ -11,6 +11,8 @@ import Button from '../../common/buttons'; import WrapToLines from '../../common/wrap-to-lines'; import { Link } from 'react-router'; +import type { HTMLElement } from '../../../types'; + type AuthState = { email: string, password: string, @@ -49,7 +51,7 @@ export default class Auth extends Component { }); } - render(): Element { + render(): HTMLElement { const { email, password, username } = this.state; return ( diff --git a/src/components/pages/checkout/checkout.css b/src/components/pages/checkout/checkout.css new file mode 100644 index 00000000..55bbf12d --- /dev/null +++ b/src/components/pages/checkout/checkout.css @@ -0,0 +1,28 @@ + +@import "media-queries.css"; + +.checkout { + text-align: center; + margin-top: 45px; + + lost-column: 9/11; + lost-offset: -1/11; +} + +.logo { + width: 45px; + height: 45px; + margin: 0 auto; + margin-bottom: 40px; +} + +@media (--medium-viewport) { + .checkout { + margin-top: 10px; + } + + .logo { + width: 40px; + height: 40px; + } +} diff --git a/src/components/pages/checkout/checkout.jsx b/src/components/pages/checkout/checkout.jsx new file mode 100644 index 00000000..b06e2b4c --- /dev/null +++ b/src/components/pages/checkout/checkout.jsx @@ -0,0 +1,23 @@ +/* @flow */ + +import React, { PropTypes } from 'react'; +import cssModules from 'react-css-modules'; +import styles from './checkout.css'; + +import Icon from '../../common/icon'; +import Shipping from './shipping'; + +const Checkout = () => { + return ( +
+ + +
+ ); +}; + +Checkout.propTypes = { + children: PropTypes.node, +}; + +export default cssModules(Checkout, styles); diff --git a/src/components/pages/checkout/shipping.jsx b/src/components/pages/checkout/shipping.jsx new file mode 100644 index 00000000..ec169d1a --- /dev/null +++ b/src/components/pages/checkout/shipping.jsx @@ -0,0 +1,24 @@ + +/* @flow */ + +import React from 'react'; +import EditableBlock from '../../editable-block'; +import cssModules from 'react-css-modules'; +import styles from './checkout.css'; + +type ShippingProps = { + isEditing: ?boolean; +}; + +const Shipping = (props: ShippingProps) => { + return ( + + ); +}; + +export default cssModules(Shipping, styles); diff --git a/src/css/colors.css b/src/css/colors.css index 7f570ad9..e7d3f188 100644 --- a/src/css/colors.css +++ b/src/css/colors.css @@ -10,4 +10,5 @@ :root { --color-error: var(--redish); + --color-panels-bg: #F4F4F4; } diff --git a/src/modules/cart.js b/src/modules/cart.js new file mode 100644 index 00000000..8df452cb --- /dev/null +++ b/src/modules/cart.js @@ -0,0 +1,10 @@ + +import {createReducer} from 'redux-act'; + +const initialState = { + skus: [], +}; + +const reducer = createReducer(void 0, initialState); + +export default reducer; diff --git a/src/modules/checkout.js b/src/modules/checkout.js new file mode 100644 index 00000000..0d7b0680 --- /dev/null +++ b/src/modules/checkout.js @@ -0,0 +1,9 @@ + +import {createReducer} from 'redux-act'; + +const initialState = { +}; + +const reducer = createReducer(void 0, initialState); + +export default reducer; diff --git a/src/modules/index.js b/src/modules/index.js index f0d40b7e..afecd8e2 100644 --- a/src/modules/index.js +++ b/src/modules/index.js @@ -2,13 +2,16 @@ import { combineReducers } from 'redux'; import { routeReducer } from 'react-router-redux'; import {reducer as formReducer} from 'redux-form'; - +import cart from './cart'; +import checkout from './checkout'; import categories from './categories'; const reducer = combineReducers({ routing: routeReducer, form: formReducer, categories, + cart, + checkout, }); export default reducer; diff --git a/src/routes.jsx b/src/routes.jsx index 51dec8ec..64dc9588 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -8,6 +8,8 @@ import Login from './components/pages/auth/login'; import SignUp from './components/pages/auth/signup'; import RestorePassword from './components/pages/auth/restore-password'; import ResetPassword from './components/pages/auth/reset-password'; + +import Checkout from './components/pages/checkout/checkout'; import Grid from './components/pages/grid'; const routes = ( @@ -21,6 +23,7 @@ const routes = ( + ); diff --git a/src/types.js b/src/types.js new file mode 100644 index 00000000..bd4c8dfc --- /dev/null +++ b/src/types.js @@ -0,0 +1,6 @@ + +import React from 'react'; + +export function HTMLElement(element) { + return React.isValidElement(element); +} diff --git a/tasks/server.js b/tasks/server.js index b42725e6..6023797b 100644 --- a/tasks/server.js +++ b/tasks/server.js @@ -2,6 +2,7 @@ /* eslint camelcase: 0 */ +const _ = require('lodash'); const child_process = require('child_process'); const runSequence = require('run-sequence'); @@ -14,14 +15,22 @@ function affectsServer(task) { module.exports = function(gulp) { let node = null; + function killServer(cb) { + if (node) { + node.once('close', cb); + node.kill(); + node = null; + } else { + cb(); + } + } + let affectTasksRunning = 0; function checkForPause(e) { if (e.task in affectsServerTasks) { affectTasksRunning++; - process.nextTick(function() { - runSequence('server.stop'); - }); + killServer(_.noop); } } @@ -38,15 +47,7 @@ module.exports = function(gulp) { } } - gulp.task('server.stop', function(cb) { - if (node) { - node.once('close', cb); - node.kill(); - node = null; - } else { - cb(); - } - }); + gulp.task('server.stop', killServer); gulp.task('server.invalidate', function(cb) { if (node) { @@ -84,9 +85,12 @@ module.exports = function(gulp) { gulp.watch(['server/**.*.js', 'src/server.jsx'], ['server.restart']); }); - process.on('exit', function() { + function silentlyKill() { if (node) node.kill(); - }); + } + + process.on('exit', silentlyKill); + process.on('uncaughtException', silentlyKill); }; module.exports.affectsServer = affectsServer;