├── .babelrc ├── .editorconfig ├── .gitignore ├── README.md ├── app ├── actions │ └── actions.js ├── app.css ├── app.html ├── app.jsx ├── components │ ├── data-table.jsx │ ├── data-table.test.js │ ├── page-navigator.jsx │ ├── page-navigator.test.js │ └── search-bar.jsx ├── containers │ └── app.jsx └── reducers │ ├── current-page.js │ ├── filter-text.js │ ├── item-properties.js │ ├── items-per-page.js │ ├── items.js │ ├── sorting-property.js │ └── state.js ├── dev-server.js ├── jsconfig.json ├── package.json ├── webpack.config.dev.js └── webpack.config.prod.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "env": { 4 | "development": { 5 | "plugins": [ 6 | ["react-transform", { 7 | "transforms": [{ 8 | "transform": "react-transform-hmr", 9 | "imports": ["react"], 10 | "locals": ["module"] 11 | }, { 12 | "transform": "react-transform-catch-errors", 13 | "imports": ["react", "redbox-react"] 14 | }] 15 | }] 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | 5 | indent_style = space 6 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | www -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Overview 2 | 3 | A simple data table with item filtering, sorting, pagination, and removal. Built with React, Redux and ImmutableJS. Demo: http://lewisl9029.github.io/redux-data-table/ 4 | 5 | Note: This is only a demo app that I built over the weekend as a part of the interview process at a company I was applying to. It's not yet ready to be used as an external component (I'll take some time to clean this up and publish on npm in the near future). 6 | -------------------------------------------------------------------------------- /app/actions/actions.js: -------------------------------------------------------------------------------- 1 | import { Seq, Map, OrderedMap } from 'immutable'; 2 | 3 | export const UPDATE_FILTER = 'UPDATE_FILTER'; 4 | export function updateFilter(filterText) { 5 | return { type: UPDATE_FILTER, filterText }; 6 | } 7 | 8 | export const SWITCH_PAGE = 'SWITCH_PAGE'; 9 | export function switchPage(page) { 10 | return { type: SWITCH_PAGE, page }; 11 | } 12 | 13 | export const SORT_ITEMS = 'SORT_ITEMS'; 14 | export function sortItems(sortingProperty) { 15 | return { type: SORT_ITEMS, sortingProperty }; 16 | } 17 | 18 | export const REMOVE_ITEM = 'REMOVE_ITEM'; 19 | export function removeItem(itemId) { 20 | return { type: REMOVE_ITEM, itemId }; 21 | } 22 | 23 | export const REVERSE_ITEMS = 'REVERSE_ITEMS'; 24 | export function reverseItems() { 25 | return { type: REVERSE_ITEMS }; 26 | } 27 | 28 | export const UPDATE_ITEMS = 'UPDATE_ITEMS'; 29 | export function updateItems(items) { 30 | return { type: UPDATE_ITEMS, items }; 31 | } -------------------------------------------------------------------------------- /app/app.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Raleway:400,300,600); 2 | 3 | button:disabled { 4 | opacity: 0.5; 5 | } 6 | 7 | #ribbon-container { 8 | pointer-events: none; 9 | position: absolute; 10 | top: 0; 11 | right: 0; 12 | overflow: hidden; 13 | height: 190px; 14 | width: 190px; 15 | } 16 | 17 | #ribbon { 18 | pointer-events: all; 19 | z-index: 2; 20 | position: relative; 21 | right: -30px; 22 | top: 30px; 23 | white-space: nowrap; 24 | transform: rotate(45deg); 25 | } 26 | 27 | #ribbon a { 28 | padding: 0 40px; 29 | background-color: white; 30 | } 31 | 32 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {%=o.htmlWebpackPlugin.options.title || 'Webpack App'%} 6 | {% if (o.htmlWebpackPlugin.files.favicon) { %} 7 | 8 | {% } %} 9 | {% for (var css in o.htmlWebpackPlugin.files.css) { %} 10 | 11 | {% } %} 12 | 13 | 14 |
15 | 21 |
22 |
23 | {% for (var chunk in o.htmlWebpackPlugin.files.chunks) { %} 24 | 25 | {% } %} 26 | 27 | -------------------------------------------------------------------------------- /app/app.jsx: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import 'normalize.css'; 4 | import 'skeleton-css-webpack'; 5 | import 'babel-polyfill'; 6 | import React from 'react'; 7 | import { render } from 'react-dom'; 8 | import { Provider } from 'react-redux'; 9 | import { createStore, applyMiddleware } from 'redux'; 10 | import createLogger from 'redux-logger'; 11 | import thunkMiddleware from 'redux-thunk'; 12 | 13 | import './app.css'; 14 | import state from './reducers/state'; 15 | import App from './containers/app'; 16 | 17 | const loggerMiddleware = createLogger(); 18 | 19 | const createStoreWithMiddleware = applyMiddleware( 20 | thunkMiddleware, 21 | loggerMiddleware 22 | )(createStore); 23 | 24 | let store = createStoreWithMiddleware(state); 25 | 26 | let app = document.getElementById('app'); 27 | 28 | render( 29 | 30 | 31 | , 32 | app 33 | ); -------------------------------------------------------------------------------- /app/components/data-table.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SearchBar from './search-bar'; 4 | import PageNavigator from './page-navigator'; 5 | 6 | export function isPartialMatch(text, filter) { 7 | return text.toLowerCase().indexOf(filter.toLowerCase()) !== -1; 8 | } 9 | 10 | export function getBeginningIndex(currentPage, itemsPerPage) { 11 | return (currentPage - 1) * itemsPerPage; 12 | } 13 | 14 | export function getEndingIndex(currentPage, itemsPerPage, numberOfItems) { 15 | return Math.min(currentPage * itemsPerPage, numberOfItems); 16 | } 17 | 18 | let DataTable = ({ 19 | currentPage, 20 | filterText, 21 | itemsPerPage, 22 | items, 23 | itemProperties, 24 | removeItem, 25 | reverseItems, 26 | sortItems, 27 | sortingProperty, 28 | switchPage, 29 | updateFilter 30 | }) => { 31 | //TODO: use reselect's memoized selectors for filtering and pagination 32 | let filteredItems = items 33 | //TODO: refactor to allow filtering by all parameters 34 | .filter(item => isPartialMatch(item.get('name'), filterText)); 35 | 36 | let beginningIndex = getBeginningIndex(currentPage, itemsPerPage); 37 | let endingIndex = getEndingIndex(currentPage, itemsPerPage, filteredItems.size); 38 | 39 | let visibleItems = filteredItems.slice(beginningIndex, endingIndex); 40 | 41 | return ( 42 |
0 ? 44 | { display: 'block' } : 45 | { display: 'none' } 46 | }> 47 |
48 | 50 |
51 | 56 | 57 | 58 | 61 | 64 | 67 | 70 | 71 | 72 | 73 | { 74 | itemProperties.valueSeq().map(itemProperty => ( 75 | 90 | )) 91 | } 92 | 93 | 94 | 95 | 96 | { 97 | visibleItems.valueSeq().map(item => ( 98 | 99 | { 100 | itemProperties.valueSeq().map(itemProperty => ( 101 | 104 | )) 105 | } 106 | 126 | 127 | )) 128 | } 129 | 130 |
sortingProperty !== itemProperty.get('id') ? 84 | sortItems(itemProperty.get('id')) : 85 | reverseItems() 86 | } 87 | > 88 | {itemProperty.get('description')} 89 |
102 | {item.get(itemProperty.get('id'))} 103 | 109 | 125 |
131 |
132 | ); 133 | }; 134 | 135 | export default DataTable; 136 | -------------------------------------------------------------------------------- /app/components/data-table.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { isPartialMatch, getBeginningIndex, getEndingIndex } from './data-table'; 3 | 4 | describe('table filtering function', () => { 5 | it('should be able to match partial strings', () => { 6 | expect( 7 | isPartialMatch('haystack needle haystack', 'needle ') 8 | ).to.be.true; 9 | }); 10 | 11 | it('should be able to match any string against empty string filter', () => { 12 | expect( 13 | isPartialMatch('haystack needle haystack', '') 14 | ).to.be.true; 15 | 16 | expect( 17 | isPartialMatch('', '') 18 | ).to.be.true; 19 | }); 20 | 21 | it('should be able to match partial strings case-insensitively', () => { 22 | expect( 23 | isPartialMatch('haystack needle haystack', 'NEEDLE') 24 | ).to.be.true; 25 | 26 | expect( 27 | isPartialMatch('haystack NEEDLE haystack', 'needle') 28 | ).to.be.true; 29 | }); 30 | }); 31 | 32 | describe('table data indexing functions', () => { 33 | it('should be correct for regular inputs', () => { 34 | expect( 35 | getBeginningIndex(1, 5) 36 | ).to.equal(0); 37 | 38 | expect( 39 | getBeginningIndex(5, 5) 40 | ).to.equal(20); 41 | 42 | expect( 43 | getEndingIndex(1, 10, 30) 44 | ).to.equal(10); 45 | 46 | expect( 47 | getEndingIndex(3, 10, 30) 48 | ).to.equal(30); 49 | }); 50 | 51 | it('should be correct for extreme inputs', () => { 52 | expect( 53 | getEndingIndex(4, 10, 35) 54 | ).to.equal(35); 55 | }); 56 | }); -------------------------------------------------------------------------------- /app/components/page-navigator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Map, Range } from 'immutable'; 3 | 4 | export function getNumberOfPages(numberOfItems, itemsPerPage) { 5 | // Ensure there is at least 1 page even when there are 0 visible items 6 | return Math.max(1, Math.ceil(numberOfItems / itemsPerPage)); 7 | } 8 | 9 | let PageNavigator = ({itemsPerPage, filteredItems, currentPage, switchPage}) => { 10 | let numberOfPages = getNumberOfPages(filteredItems.size, itemsPerPage); 11 | // Range is inclusive for begin, non-inclusive for end 12 | let pages = Range(1, numberOfPages + 1); 13 | return ( 14 |
15 |
16 | 27 |
28 |
29 | 45 |
46 |
47 | 58 |
59 |
60 | ); 61 | }; 62 | 63 | export default PageNavigator; -------------------------------------------------------------------------------- /app/components/page-navigator.test.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { getNumberOfPages } from './page-navigator'; 3 | 4 | describe('page navigator max page number calculation', () => { 5 | it('should be correct for regular values', () => { 6 | expect( 7 | getNumberOfPages(20, 10) 8 | ).to.equal(2); 9 | 10 | expect( 11 | getNumberOfPages(21, 10) 12 | ).to.equal(3); 13 | }); 14 | 15 | it('should be return at least 1 page', () => { 16 | expect( 17 | getNumberOfPages(8, 10) 18 | ).to.equal(1); 19 | 20 | expect( 21 | getNumberOfPages(0, 10) 22 | ).to.equal(1); 23 | }); 24 | }); -------------------------------------------------------------------------------- /app/components/search-bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | let SearchBar = ({ updateFilter }) => ( 4 |
5 |
6 | updateFilter(event.target.value)} 14 | /> 15 |
16 |
17 | ); 18 | 19 | export default SearchBar; -------------------------------------------------------------------------------- /app/containers/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { connect } from 'react-redux'; 4 | 5 | import * as actions from '../actions/actions'; 6 | import DataTable from '../components/data-table'; 7 | 8 | let App = ({ 9 | dispatch, 10 | currentPage, 11 | filename, 12 | filterText, 13 | itemsPerPage, 14 | items, 15 | itemProperties, 16 | sortingProperty 17 | }) => ( 18 |
21 |

Redux Data Table Demo

22 | dispatch(actions.removeItem(itemId))} 29 | reverseItems={() => dispatch(actions.reverseItems())} 30 | sortingProperty={sortingProperty} 31 | sortItems={sortingProperty => dispatch(actions.sortItems(sortingProperty))} 32 | switchPage={page => dispatch(actions.switchPage(page))} 33 | updateFilter={filterText => dispatch(actions.updateFilter(filterText))} /> 34 |
35 | ); 36 | 37 | export default connect(state => ({ 38 | currentPage: state.get('currentPage'), 39 | filterText: state.get('filterText'), 40 | itemProperties: state.get('itemProperties'), 41 | items: state.get('items'), 42 | itemsPerPage: state.get('itemsPerPage'), 43 | sortingProperty: state.get('sortingProperty') 44 | }))(App); -------------------------------------------------------------------------------- /app/reducers/current-page.js: -------------------------------------------------------------------------------- 1 | import { SWITCH_PAGE, UPDATE_FILTER, UPDATE_ITEMS } from '../actions/actions'; 2 | 3 | let initialState = 1; 4 | 5 | let currentPage = (state = initialState, action) => { 6 | switch (action.type) { 7 | case SWITCH_PAGE: 8 | return action.page; 9 | // Reset to first page when filtering to avoid out of bounds pages 10 | case UPDATE_FILTER: 11 | return initialState; 12 | // Reset to first page when updating data to avoid out of bounds pages 13 | case UPDATE_ITEMS: 14 | return initialState; 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export default currentPage; -------------------------------------------------------------------------------- /app/reducers/filter-text.js: -------------------------------------------------------------------------------- 1 | import { UPDATE_FILTER } from '../actions/actions'; 2 | 3 | let initialState = ''; 4 | 5 | let filterText = (state = initialState, action) => { 6 | switch (action.type) { 7 | case UPDATE_FILTER: 8 | return action.filterText; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default filterText; -------------------------------------------------------------------------------- /app/reducers/item-properties.js: -------------------------------------------------------------------------------- 1 | import { Map, OrderedMap } from 'immutable'; 2 | 3 | //TODO: refactor as props on the exported component for external use 4 | let initialState = OrderedMap({ 5 | 'name': Map({ id: 'name', description: 'Name'}), 6 | 'job': Map({ id: 'job', description: 'Job Title'}), 7 | 'salary': Map({ id: 'salary', description: 'Salary'}) 8 | }); 9 | 10 | let itemProperties = (state = initialState, action) => { 11 | switch (action.type) { 12 | default: 13 | return state; 14 | } 15 | }; 16 | 17 | export default itemProperties; -------------------------------------------------------------------------------- /app/reducers/items-per-page.js: -------------------------------------------------------------------------------- 1 | //TODO: allow this to be customized 2 | let initialState = 10; 3 | 4 | let itemsPerPage = (state = initialState, action) => { 5 | switch (action.type) { 6 | default: 7 | return state; 8 | } 9 | }; 10 | 11 | export default itemsPerPage; -------------------------------------------------------------------------------- /app/reducers/items.js: -------------------------------------------------------------------------------- 1 | import { Range, Map, OrderedMap } from 'immutable'; 2 | import faker from 'faker'; 3 | 4 | import { 5 | SORT_ITEMS, 6 | REMOVE_ITEM, 7 | REVERSE_ITEMS, 8 | UPDATE_ITEMS 9 | } from '../actions/actions'; 10 | 11 | //TODO: refactor as props on the exported component for external use 12 | let fakeItems = Range(0, 1000) 13 | .map(id => [id, Map({ 14 | id, 15 | name: faker.name.findName(), 16 | job: faker.name.jobTitle(), 17 | salary: faker.random.number() 18 | })]); 19 | 20 | let initialState = OrderedMap(fakeItems); 21 | 22 | let items = (state = initialState, action) => { 23 | switch (action.type) { 24 | case SORT_ITEMS: 25 | return state.sortBy(item => item.get(action.sortingProperty)); 26 | case REVERSE_ITEMS: 27 | return state.reverse(); 28 | case REMOVE_ITEM: 29 | return state.remove(action.itemId); 30 | //TODO: support updates to individual items eventually 31 | case UPDATE_ITEMS: 32 | return action.items; 33 | default: 34 | return state; 35 | } 36 | }; 37 | 38 | export default items; -------------------------------------------------------------------------------- /app/reducers/sorting-property.js: -------------------------------------------------------------------------------- 1 | import { SORT_ITEMS } from '../actions/actions'; 2 | 3 | let initialState = 'id'; 4 | 5 | let sortingProperty = (state = initialState, action) => { 6 | switch (action.type) { 7 | case SORT_ITEMS: 8 | return action.sortingProperty; 9 | default: 10 | return state; 11 | } 12 | }; 13 | 14 | export default sortingProperty; -------------------------------------------------------------------------------- /app/reducers/state.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux-immutablejs'; 2 | import { Map } from 'immutable'; 3 | 4 | import currentPage from './current-page'; 5 | import filterText from './filter-text'; 6 | import itemProperties from './item-properties'; 7 | import items from './items'; 8 | import itemsPerPage from './items-per-page'; 9 | import sortingProperty from './sorting-property'; 10 | 11 | export default combineReducers({ 12 | currentPage, 13 | filterText, 14 | itemProperties, 15 | items, 16 | itemsPerPage, 17 | sortingProperty 18 | }); -------------------------------------------------------------------------------- /dev-server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var webpack = require('webpack'); 4 | var config = require('./webpack.config.dev'); 5 | 6 | var app = express(); 7 | var compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | noInfo: true, 11 | publicPath: config.output.publicPath 12 | })); 13 | 14 | app.use(require('webpack-hot-middleware')(compiler)); 15 | 16 | app.get('*', function(req, res) { 17 | res.sendFile(path.join(__dirname, 'app/app.html')); 18 | }); 19 | 20 | app.listen(8080, 'localhost', function(err) { 21 | if (err) { 22 | console.log(err); 23 | return; 24 | } 25 | 26 | console.log('Listening at http://localhost:8080'); 27 | }); -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "compilerOptions": { 4 | "target": "ES6", 5 | "module": "commonjs" 6 | } 7 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-data-table", 3 | "version": "0.0.1", 4 | "description": "Simple React + Redux + ImmutableJS data table with item filtering, sorting, pagination and removal.", 5 | "main": "app/app.jsx", 6 | "scripts": { 7 | "build": "node_modules/.bin/webpack --config webpack.config.prod.js", 8 | "start": "node dev-server.js", 9 | "test": "node_modules/.bin/mocha --reporter spec --compilers js:babel-core/register app/**/*.test.js", 10 | "test:watch": "npm test -- --watch" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/lewisl9029/redux-data-table.git" 15 | }, 16 | "author": "Lewis Liu", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/lewisl9029/redux-data-table/issues" 20 | }, 21 | "homepage": "https://github.com/lewisl9029/redux-data-table#readme", 22 | "dependencies": { 23 | "babel-polyfill": "^6.3.14", 24 | "faker": "^3.0.1", 25 | "immutable": "^3.7.5", 26 | "normalize.css": "^3.0.3", 27 | "react": "^0.14.3", 28 | "react-dom": "^0.14.3", 29 | "react-redux": "^4.0.0", 30 | "redux": "^3.0.4", 31 | "redux-immutablejs": "0.0.7", 32 | "redux-logger": "^2.2.1", 33 | "redux-thunk": "^1.0.0", 34 | "skeleton-css-webpack": "^2.0.4" 35 | }, 36 | "devDependencies": { 37 | "babel-core": "^6.3.17", 38 | "babel-loader": "^6.2.0", 39 | "babel-plugin-react-transform": "^2.0.0-beta1", 40 | "babel-preset-es2015": "^6.3.13", 41 | "babel-preset-react": "^6.3.13", 42 | "chai": "^3.4.1", 43 | "chai-immutable": "^1.5.3", 44 | "css-loader": "^0.23.0", 45 | "express": "^4.13.3", 46 | "extract-text-webpack-plugin": "^0.9.1", 47 | "html-webpack-plugin": "^1.7.0", 48 | "mocha": "^2.3.4", 49 | "react-transform-catch-errors": "^1.0.0", 50 | "react-transform-hmr": "^1.0.1", 51 | "redbox-react": "^1.2.0", 52 | "style-loader": "^0.13.0", 53 | "webpack": "^1.12.9", 54 | "webpack-dev-middleware": "^1.4.0", 55 | "webpack-hot-middleware": "^2.6.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // Hot reloading doesn't work right now due to the fact that 3 | // React v0.14's stateless components [1] aren't supported yet [2] 4 | // [1] https://facebook.github.io/react/blog/2015/10/07/react-v0.14.html#stateless-functional-components 5 | // [2] https://github.com/gaearon/babel-plugin-react-transform/issues/57 6 | let path = require('path'); 7 | let webpack = require('webpack'); 8 | let HtmlWebpackPlugin = require('html-webpack-plugin'); 9 | let ExtractTextPlugin = require('extract-text-webpack-plugin'); 10 | 11 | module.exports = { 12 | devtool: 'source-map', 13 | entry: [ 14 | 'webpack-hot-middleware/client', 15 | './app/app' 16 | ], 17 | module: { 18 | loaders: [{ 19 | test: /\.jsx?$/, 20 | include: path.join(__dirname, 'app'), 21 | loaders: [ 22 | 'babel' 23 | ] 24 | }, { 25 | test: /\.css$/, 26 | loader: ExtractTextPlugin.extract("style-loader", "css-loader") 27 | }] 28 | }, 29 | resolve: { 30 | extensions: ['', '.js', '.jsx'] 31 | }, 32 | output: { 33 | path: path.join(__dirname, 'www'), 34 | filename: 'app.js' 35 | }, 36 | plugins: [ 37 | new webpack.HotModuleReplacementPlugin(), 38 | new webpack.NoErrorsPlugin(), 39 | new HtmlWebpackPlugin({ 40 | title: 'Redux Data Table Demo', 41 | template: './app/app.html' 42 | }), 43 | new ExtractTextPlugin("[name].css") 44 | ] 45 | }; -------------------------------------------------------------------------------- /webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let path = require('path'); 4 | let webpack = require('webpack'); 5 | let HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | let ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | 8 | module.exports = { 9 | devtool: 'source-map', 10 | entry: [ 11 | './app/app' 12 | ], 13 | module: { 14 | loaders: [{ 15 | test: /\.jsx?$/, 16 | include: path.join(__dirname, 'app'), 17 | loaders: [ 18 | 'babel' 19 | ] 20 | }, { 21 | test: /\.css$/, 22 | loader: ExtractTextPlugin.extract("style-loader", "css-loader") 23 | }] 24 | }, 25 | resolve: { 26 | extensions: ['', '.js', '.jsx'] 27 | }, 28 | output: { 29 | path: path.resolve(__dirname, 'www'), 30 | filename: 'app.js' 31 | }, 32 | plugins: [ 33 | new webpack.optimize.OccurenceOrderPlugin(), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compressor: { 36 | warnings: false 37 | } 38 | }), 39 | new HtmlWebpackPlugin({ 40 | title: 'Redux Data Table Demo', 41 | template: './app/app.html' 42 | }), 43 | new ExtractTextPlugin("[name].css") 44 | ] 45 | }; --------------------------------------------------------------------------------