diff --git a/README.md b/README.md index fca208d97..d037fd291 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,5 @@ # Gitify -[![travis][travis-image]][travis-url] -[![codecov][codecov-image]][codecov-url] -[![slack][slack-image]][slack-url] - -### GitHub Notifications on your menu bar. +[![travis][travis-image]][travis-url] [![codecov][codecov-image]][codecov-url] [![slack][slack-image]][slack-url] ![Gitify](images/press.png) @@ -20,8 +16,8 @@ It has been a while since this app was made so I decided to give it a good revam - [x] Update Bootstrap to version 4 - Which means move from LESS to SCSS. - [x] Rewrite tests with Mocha - Since gitify is moving from Reflux to Redux, all tests have to be rewritten. - [x] Move to Codecov for coverage with new tests -- [ ] Revamp the UI. From Scratch? -- [ ] Rebranding - New Logo! Fresh stuff! +- [x] Rebranding - New Logo! Fresh stuff! +- [x] Revamp the UI. From Scratch? If you would like to help let me know! There is a slack channel for gitify in the [atom](http://atomio.slack.com) team. See badge on the header. diff --git a/images/press.png b/images/press.png index 45fc187a7..0990a01cd 100644 Binary files a/images/press.png and b/images/press.png differ diff --git a/package.json b/package.json index 8f4d11d01..7db7a30c0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "webpack", "watch": "webpack --progress --colors --watch", "release-js": "webpack --config webpack.rel.config.js", - "package": "electron-packager . Gitify --overwrite --platform=darwin --arch=x64 --version=0.35.4 --asar=true --icon=images/app-icon.icns --prune --ignore='src' --ignore='coverage'", + "package": "electron-packager . Gitify --overwrite --platform=darwin --arch=x64 --version=1.0.2 --asar=true --icon=images/app-icon.icns --prune --ignore='src' --ignore='coverage'", "codesign": "bash scripts/codesign.bash", "dist": "npm run release-js && npm run package && npm run codesign", "lint-js": "eslint 'src/js/' 'src/js/app.js' 'main.js'", @@ -51,6 +51,8 @@ "dependencies": { "auto-launch": "=2.0.1", "bootstrap": "=4.0.0-alpha.2", + "electron-gh-releases": "=2.0.2", + "electron-positioner": "=3.0.0", "font-awesome": "=4.6.1", "history": "=2.1.1", "malarkey": "=1.3.3", @@ -70,7 +72,6 @@ "redux-storage": "=4.0.0", "redux-storage-decorator-filter": "=1.1.3", "redux-storage-engine-localstorage": "=1.1.0", - "reloading": "=0.0.6", "underscore": "=1.8.3" }, "devDependencies": { @@ -83,10 +84,8 @@ "babel-preset-stage-0": "=6.5.0", "chai": "=3.5.0", "css-loader": "=0.23.1", - "electron-gh-releases": "=2.0.2", "electron-packager": "=7.0.1", "electron-prebuilt": "=1.0.2", - "electron-positioner": "=3.0.0", "enzyme": "=2.3.0", "eslint": "=2.9.0", "eslint-plugin-react": "=5.1.1", diff --git a/src/js/__tests__/components/login.js b/src/js/__tests__/components/login.js index 5f60ffb28..14018a6f8 100644 --- a/src/js/__tests__/components/login.js +++ b/src/js/__tests__/components/login.js @@ -48,7 +48,7 @@ describe('components/login.js', function () { const { wrapper } = setup(props); expect(wrapper).to.exist; - expect(wrapper.find('.desc').text()).to.equal('GitHub notifications in your menu bar.'); + expect(wrapper.find('.desc').text()).to.contain('in your menu bar.'); }); @@ -206,7 +206,6 @@ describe('components/login.js', function () { const { wrapper, context } = setup(props); expect(wrapper).to.exist; - expect(wrapper.find('.desc').text()).to.equal('GitHub notifications in your menu bar.'); wrapper.setProps({ token: 'HELLO' diff --git a/src/js/__tests__/components/navigation.js b/src/js/__tests__/components/navigation.js index c3f6c9fd3..31845158a 100644 --- a/src/js/__tests__/components/navigation.js +++ b/src/js/__tests__/components/navigation.js @@ -61,7 +61,6 @@ describe('components/navigation.js', function () { expect(Navigation.prototype.componentDidMount).to.have.been.calledOnce; expect(wrapper.find('.fa-refresh').length).to.equal(1); expect(wrapper.find('.fa-refresh').first().hasClass('fa-spin')).to.be.false; - expect(wrapper.find('.fa-sign-out').length).to.equal(1); expect(wrapper.find('.fa-cog').length).to.equal(1); expect(wrapper.find('.fa-search').length).to.equal(1); expect(wrapper.find('.fa-power-off').length).to.equal(0); @@ -111,7 +110,6 @@ describe('components/navigation.js', function () { expect(wrapper).to.exist; expect(Navigation.prototype.componentDidMount).to.have.been.calledOnce; expect(wrapper.find('.fa-refresh').length).to.equal(0); - expect(wrapper.find('.fa-sign-out').length).to.equal(0); expect(wrapper.find('.fa-cog').length).to.equal(0); expect(wrapper.find('.fa-search').length).to.equal(0); expect(wrapper.find('.fa-power-off').length).to.equal(1); @@ -141,7 +139,6 @@ describe('components/navigation.js', function () { expect(Navigation.prototype.componentDidMount).to.have.been.calledOnce; expect(wrapper.find('.fa-refresh').length).to.equal(1); expect(wrapper.find('.fa-refresh').first().hasClass('fa-spin')).to.be.false; - expect(wrapper.find('.fa-sign-out').length).to.equal(1); expect(wrapper.find('.fa-cog').length).to.equal(1); expect(wrapper.find('.fa-search').length).to.equal(1); expect(wrapper.find('.fa-power-off').length).to.equal(0); @@ -168,7 +165,6 @@ describe('components/navigation.js', function () { expect(wrapper).to.exist; expect(wrapper.find('.fa-refresh').length).to.equal(0); - expect(wrapper.find('.fa-sign-out').length).to.equal(0); expect(wrapper.find('.fa-power-off').length).to.equal(1); wrapper.find('.fa-power-off').simulate('click'); @@ -193,7 +189,7 @@ describe('components/navigation.js', function () { expect(wrapper).to.exist; expect(wrapper.find('.fa-power-off').length).to.equal(1); - wrapper.find('.logo').simulate('click'); + wrapper.find('.navbar-brand').simulate('click'); expect(shell.openExternal).to.have.been.calledOnce; expect(shell.openExternal).to.have.been.calledWith('http://www.github.com/ekonstantinidis/gitify'); @@ -224,42 +220,6 @@ describe('components/navigation.js', function () { }); - it('should press the logout', function () { - - const props = { - logout: sinon.spy(), - toggleSearch: sinon.spy(), - isFetching: false, - notifications: notifications.length, - showSearch: true, - token: 'IMLOGGEDIN', - location: { - pathname: '/settings' - } - }; - - const { wrapper, context } = setup(props); - - expect(wrapper).to.exist; - expect(wrapper.find('.fa-cog').length).to.equal(1); - - wrapper.find('.fa-sign-out').simulate('click'); - - expect(props.logout).to.have.been.calledOnce; - expect(props.toggleSearch).to.have.been.calledOnce; - - expect(ipcRenderer.send).to.have.been.calledOnce; - expect(ipcRenderer.send).to.have.been.calledWith('update-icon', 'IconPlain'); - - expect(context.router.replace).to.have.been.calledOnce; - expect(context.router.replace).to.have.been.calledWith('/login'); - - context.router.replace.reset(); - props.logout.reset(); - props.toggleSearch.reset(); - - }); - it('should go to settings from home', function () { const props = { diff --git a/src/js/__tests__/components/oops.js b/src/js/__tests__/components/oops.js index a6a4f782e..82e42bfee 100644 --- a/src/js/__tests__/components/oops.js +++ b/src/js/__tests__/components/oops.js @@ -20,7 +20,7 @@ describe('components/oops.js', function () { const { wrapper } = setup(); expect(wrapper).to.exist; - expect(wrapper.find('h3').text()).to.equal('Oops something went wrong.'); + expect(wrapper.find('h2').text()).to.equal('Oops something went wrong.'); }); diff --git a/src/js/__tests__/components/settings.js b/src/js/__tests__/components/settings.js index 090fa01d2..7db2effeb 100644 --- a/src/js/__tests__/components/settings.js +++ b/src/js/__tests__/components/settings.js @@ -4,23 +4,25 @@ import { shallow } from 'enzyme'; import sinon from 'sinon'; import Toggle from 'react-toggle'; import { SettingsPage } from '../../components/settings'; - -function setup() { - const props = { - updateSetting: sinon.spy(), - fetchNotifications: sinon.spy(), - settings: { - participating: false, - playSound: true, - showNotifications: true, - markOnClick: false, - openAtStartup: false +const ipcRenderer = window.require('electron').ipcRenderer; + +const options = { + context: { + location: { + pathname: '' + }, + router: { + push: sinon.spy(), + replace: sinon.spy() } - }; + } +}; - const wrapper = shallow(); +function setup(props) { + const wrapper = shallow(, options); return { + context: options.context, props: props, wrapper: wrapper, }; @@ -28,19 +30,50 @@ function setup() { describe('components/settings.js', function () { + beforeEach(function() { + ipcRenderer.send.reset(); + }); + it('should render itself & its children', function () { - const { wrapper } = setup(); + const props = { + updateSetting: sinon.spy(), + fetchNotifications: sinon.spy(), + logout: sinon.spy(), + settings: { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false + } + }; + + const { wrapper } = setup(props); expect(wrapper).to.exist; expect(wrapper.find(Toggle).length).to.equal(5); + expect(wrapper.find('.fa-sign-out').length).to.equal(1); expect(wrapper.find('.footer').find('.text-right').text()).to.contain('Gitify - Version'); }); it('should update a setting', function () { - const { wrapper, props } = setup(); + const props = { + updateSetting: sinon.spy(), + fetchNotifications: sinon.spy(), + logout: sinon.spy(), + settings: { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false + } + }; + + const { wrapper } = setup(props); expect(wrapper).to.exist; // Note: First Toggle is "participating" @@ -58,14 +91,86 @@ describe('components/settings.js', function () { }); - it('should check for updates and quit the app', function () { + it('should check for updates ', function () { + + const props = { + updateSetting: sinon.spy(), + fetchNotifications: sinon.spy(), + logout: sinon.spy(), + settings: { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false + } + }; + + const { wrapper } = setup(props); + + expect(wrapper).to.exist; + + wrapper.find('.fa-cloud-download').parent().simulate('click'); + expect(ipcRenderer.send).to.have.been.calledOnce; + expect(ipcRenderer.send).to.have.been.calledWith('check-update'); + + }); + + it('should quit the app', function () { + + const props = { + updateSetting: sinon.spy(), + fetchNotifications: sinon.spy(), + logout: sinon.spy(), + settings: { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false + } + }; - const { wrapper } = setup(); + const { wrapper } = setup(props); expect(wrapper).to.exist; - wrapper.find('.btn-primary').simulate('click'); - wrapper.find('.btn-danger').simulate('click'); + wrapper.find('.fa-power-off').parent().simulate('click'); + expect(ipcRenderer.send).to.have.been.calledOnce; + expect(ipcRenderer.send).to.have.been.calledWith('app-quit'); + + }); + + it('should press the logout', function () { + + const props = { + updateSetting: sinon.spy(), + fetchNotifications: sinon.spy(), + logout: sinon.spy(), + settings: { + participating: false, + playSound: true, + showNotifications: true, + markOnClick: false, + openAtStartup: false + } + }; + + const { wrapper, context } = setup(props); + + expect(wrapper).to.exist; + + wrapper.find('.fa-sign-out').parent().simulate('click'); + + expect(props.logout).to.have.been.calledOnce; + + expect(ipcRenderer.send).to.have.been.calledOnce; + expect(ipcRenderer.send).to.have.been.calledWith('update-icon', 'IconPlain'); + + expect(context.router.replace).to.have.been.calledOnce; + expect(context.router.replace).to.have.been.calledWith('/login'); + + context.router.replace.reset(); }); diff --git a/src/js/components/login.js b/src/js/components/login.js index 51b2bd573..09d4f9765 100644 --- a/src/js/components/login.js +++ b/src/js/components/login.js @@ -76,9 +76,9 @@ export class LoginPage extends React.Component {
- -
GitHub notifications in your menu bar.
-
diff --git a/src/js/components/navigation.js b/src/js/components/navigation.js index 1e7d7951f..96d390bb9 100644 --- a/src/js/components/navigation.js +++ b/src/js/components/navigation.js @@ -29,16 +29,6 @@ export class Navigation extends React.Component { this.context.router.push('/settings'); } - logoutButton() { - if (this.props.showSearch) { - this.props.toggleSearch(); - } - - this.props.logout(); - this.context.router.replace('/login'); - ipcRenderer.send('update-icon', 'IconPlain'); - } - goBack() { this.context.router.push('/notifications'); } @@ -53,22 +43,25 @@ export class Navigation extends React.Component { render() { const isLoggedIn = this.props.token !== null; - var refreshIcon, logoutIcon, backIcon, settingsIcon, quitIcon, searchIcon, countLabel; - var loadingClass = this.props.isFetching ? 'fa fa-refresh fa-spin' : 'fa fa-refresh'; + const loadingClass = this.props.isFetching ? ' logo-spin' : ''; + var refreshIcon, backIcon, settingsIcon, quitIcon, searchIcon, countLabel; if (isLoggedIn) { refreshIcon = ( - - ); - logoutIcon = ( - +
  • + +
  • ); settingsIcon = ( - +
  • + +
  • ); if (this.props.notifications.length) { searchIcon = ( - +
  • + +
  • ); countLabel = ( {this.props.notifications.length} @@ -76,39 +69,41 @@ export class Navigation extends React.Component { } } else { quitIcon = ( - +
  • + +
  • ); } if (this.props.location.pathname === '/settings') { backIcon = ( - +
  • + +
  • ); settingsIcon = ( - +
  • + +
  • ); } return ( -
    -
    -
    - - {countLabel} - {refreshIcon} -
    -
    - {backIcon} - {searchIcon} - {settingsIcon} - {logoutIcon} - {quitIcon} -
    -
    -
    + ); } }; diff --git a/src/js/components/notifications.js b/src/js/components/notifications.js index 941cb4fbf..842ff5f14 100644 --- a/src/js/components/notifications.js +++ b/src/js/components/notifications.js @@ -2,7 +2,6 @@ import _ from 'underscore'; import React from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import { connect } from 'react-redux'; -import Loading from 'reloading'; const shell = window.require('electron').shell; @@ -53,10 +52,6 @@ export class NotificationsPage extends React.Component { return (
    - -
    working on it
    -
    - -

    Oops something went wrong.

    +

    Oops something went wrong.

    Couldn't get your notifications.

    {emojify(emoji, {output: 'unicode'})}

    diff --git a/src/js/components/search.js b/src/js/components/search.js index b36cbe844..a8e3759b6 100644 --- a/src/js/components/search.js +++ b/src/js/components/search.js @@ -25,20 +25,18 @@ export class SearchBar extends React.Component { } return ( -
    -
    -
    -
    - this.props.searchNotifications(evt.target.value)} - className="form-control" - type="text" - placeholder=" Search..." /> -
    -
    -
    {clearSearchIcon}
    +
    +
    + this.props.searchNotifications(evt.target.value)} + className="form-control form-control-sm" + type="text" + placeholder=" Search..." /> +
    +
    + {clearSearchIcon}
    ); diff --git a/src/js/components/settings.js b/src/js/components/settings.js index 0bd36c6fd..caf671bfb 100644 --- a/src/js/components/settings.js +++ b/src/js/components/settings.js @@ -4,7 +4,7 @@ import Toggle from 'react-toggle'; const ipcRenderer = window.require('electron').ipcRenderer; -import { fetchNotifications, updateSetting } from '../actions'; +import { fetchNotifications, updateSetting, logout } from '../actions'; export class SettingsPage extends React.Component { componentWillReceiveProps(nextProps) { @@ -17,6 +17,12 @@ export class SettingsPage extends React.Component { this.props.updateSetting(key, event.target.checked); } + logout() { + this.props.logout(); + this.context.router.replace('/login'); + ipcRenderer.send('update-icon', 'IconPlain'); + } + checkForUpdates() { ipcRenderer.send('check-update'); } @@ -31,7 +37,25 @@ export class SettingsPage extends React.Component { return (
    -
    + + +
    Show only participating
    -
    +
    Play sound
    -
    +
    Show notifications
    -
    +
    On Click, Mark as Read
    -
    +
    Open at startup
    -
    -
    - -
    -
    - -
    -
    -
    Gitify - Version: {appVersion}
    +
    Made with in Brighton.
    +
    Gitify - Version: {appVersion}
    ); } }; +SettingsPage.contextTypes = { + router: React.PropTypes.object.isRequired +}; function mapStateToProps(state) { return { @@ -107,4 +117,8 @@ function mapStateToProps(state) { }; }; -export default connect(mapStateToProps, { updateSetting, fetchNotifications })(SettingsPage); +export default connect(mapStateToProps, { + updateSetting, + fetchNotifications, + logout +})(SettingsPage); diff --git a/src/scss/app.scss b/src/scss/app.scss index d51481e63..397197cce 100644 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -13,7 +13,6 @@ $theme-black: #262626; $theme-green: #5EBA7D; $white: #FFF; -$dark-gray: darken($white, 75%); $light-gray: darken($white, 10%); $error-color: #DB423C; @@ -36,6 +35,8 @@ $octicons-font-path: "../../node_modules/octicons/octicons"; /* @group Bootstrap Overrides */ $navbar-height: 35px; +$navbar-padding-vertical: .3rem; +$navbar-padding-horizontal: 1rem; $brand-success: $theme-green; @@ -76,7 +77,7 @@ $alert-danger-text: $white; @mixin check-octicon { &.octicon-check { transition: all .4s; - color: darken($light-gray, 20%); + color: $gray-light; &:hover { cursor: pointer; @@ -114,7 +115,7 @@ html { overflow: auto; } -.search-bar + .main-container { +.search-form + .main-container { top: $navbar-height + $navbar-height; } @@ -131,7 +132,8 @@ html { } } -.right { +.right, +.text-right { text-align: right; } @@ -188,110 +190,81 @@ input { /* @end Animations */ -/* @group Loading Service */ +/* @group Navigation */ -.loading-container { - position: absolute; - top: 0; - left: 0; - z-index: 9999; - background: $theme-primary; - width: 100%; - height: 100%; - text-align: center; - - .loading-text { - position: relative; - top: 120px; - color: $white; - font-size: 20px; - -webkit-animation: loadbars 2s cubic-bezier(.645, .045, .355, 1) infinite 0s; - } -} - -@-webkit-keyframes loadbars { - 0% { - margin-top: 25px; - height: 10px; - } - - 50% { - margin-top: 0; - height: 50px; - } - - 100% { - margin-top: 25px; - height: 10px; - } -} - -/* @end Loading Service */ - - -/* @group Navbar */ - -.navigation { - background-color: $theme-primary; +.navbar { height: $navbar-height; color: $white; - div[class*="col-xs-"] { - line-height: 32px; - font-size: 16px; - - &.left { - .logo { - display: inline-block; - margin: 0 5px 2px; - max-height: 18px; - } - - .label { - margin-right: 5px; - } + .navbar-brand { + display: inline-block; + margin: 0 5px 2px; + max-height: 25px; - .fa { - margin: 0 5px; + &.logo-spin { + -webkit-animation: spin 1s infinite linear; - &.fa-refresh { - font-size: 14px; + @-webkit-keyframes spin { + 0% { + transform: rotate(0deg); } - } - } - - &.right { - .fa { - margin: 0 10px; - &.fa-chevron-left, - &.fa-search { - font-size: 14px; + 100% { + transform: rotate(360deg); } } } + } - .fa { + .label { + @include border-radius(0); - &:active, - &:hover { - color: $dark-gray; - } - } + position: relative; + top: -$navbar-padding-vertical; + left: 5px; + float: left; + margin-right: .95rem; + min-width: 1.5rem; + height: $navbar-height; + line-height: 29px; + } + + .nav-link { + padding-top: .3rem; + font-size: .95rem; } } -/* @end Navbar */ +/* @end Navigation */ /* @group Settings */ .settings { - background-color: $theme-primary; + background-color: $gray-lighter; padding: 5px 20px; - color: $white; - .row { + .nav { + display: flex; + margin: -5px -20px 20px; + + background-color: $gray-light; + padding: 5px 0; + justify-content: center; + + .nav-link { + color: $gray-dark; + font-size: 90%; + + &:active, + &:hover { + cursor: pointer; + color: $white; + } + } + } + + .setting { margin: 10px 5px; font-size: .95rem; font-weight: 300; @@ -299,18 +272,15 @@ input { .col-xs-4 { text-align: right; } - - .btn-close { - padding: 10px; - } } .footer { + margin: 10px 5px; padding-top: 5px; - font-size: 10px; + font-size: 12px; - .text-right { - padding-right: 18px; + .heart { + color: $brand-danger; } } } @@ -318,39 +288,46 @@ input { /* @end Settings */ -/* @group Component / Login */ +/* @group Page / Login */ .login { position: absolute; top: $navbar-height; left: 0; - background-color: $theme-primary; - padding-top: 30px; + background-color: $gray-lighter; + padding-top: 20px; width: 100%; height: 100%; color: $white; .logo { display: block; - margin: 10px auto 20px; - max-width: 150px; + margin: 0 auto 20px; + max-width: 115px; } .desc { - margin-bottom: 20px; + margin-bottom: 25px; text-align: center; + line-height: 22px; + color: $gray-dark; font-size: 20px; } + + .btn { + @include button-variant($white, $gray, $btn-primary-border); + padding: .55rem 1.25rem; + } } -/* @end Component / Login */ +/* @end Page / Login */ /* @group Component / Repository */ .repository { margin: 0; - background-color: $light-gray; + background-color: $gray-lighter; padding: 5px 20px; .col-xs-2, @@ -407,9 +384,9 @@ input { &.errored, &.all-read { - background-color: $theme-primary; + background-color: $gray-lighter; padding: 30px 20px; - color: $white; + color: $gray; h2 { text-align: center; @@ -439,7 +416,7 @@ input { animation: blink .9s infinite; opacity: 1; padding-left: 5px; - color: $white; + color: $gray; @keyframes blink { 0% { opacity: 1; } @@ -478,7 +455,7 @@ input { .notification { margin: 0; - border-bottom: 1px solid darken($light-gray, 10%); + border-bottom: 1px solid $gray-lighter; padding: 2px 20px; .col-xs-1, @@ -508,7 +485,7 @@ input { } &:hover { - background-color: $light-gray; + background-color: $gray-lightest; } } @@ -519,54 +496,33 @@ input { @SearchHeight: 22px; -.search-bar { +.search-form { margin: 0; - background-color: $theme-primary; - padding: 0 20px; + background-color: $gray; height: $navbar-height; text-align: center; - div[class*="col-xs-"] { - padding: 0; - } - - -webkit-animation: fadein .5s; - - @-webkit-keyframes fadein { - 0% { - opacity: 0; - } - - 100% { - opacity: 1; - } - } -} - -.form-group { - margin-bottom: 0; - .form-control { outline: none; border: 0; box-shadow: none; - background-color: $theme-primary; - padding: 0 0 0 16px; + background-color: $gray; + padding: 0 1.5rem; color: $white; font-size: 18px; &::-webkit-input-placeholder { - color: $light-gray; + color: $gray-lighter; } } -} -.octicon-x { - cursor: pointer; - text-align: center; - line-height: 36px; - color: $white; - font-size: 20px; + .octicon-x { + cursor: pointer; + text-align: center; + line-height: 28px; + color: $white; + font-size: 20px; + } } /* @end Component / Search */ diff --git a/webpack.rel.config.js b/webpack.rel.config.js index 92aea5edc..ad0ce7a92 100644 --- a/webpack.rel.config.js +++ b/webpack.rel.config.js @@ -4,6 +4,11 @@ var config = require('./webpack.config.js'); config.devtool = ''; config.plugins = config.plugins.concat([ + new webpack.DefinePlugin({ + 'process.env':{ + 'NODE_ENV': JSON.stringify('production') + } + }), new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false,