├── .gitignore ├── src ├── app │ ├── views │ │ └── index │ │ │ ├── style.sass │ │ │ └── index.jsx │ ├── index.sass │ ├── components │ │ └── image-uploader │ │ │ ├── style.sass │ │ │ └── index.jsx │ ├── history.js │ ├── localization.json │ ├── utils │ │ ├── create-action-creator.js │ │ └── create-reducer.js │ ├── actions │ │ ├── error │ │ │ ├── index.js │ │ │ └── index.test.js │ │ ├── route │ │ │ ├── index.js │ │ │ └── index.test.js │ │ └── locale │ │ │ ├── index.js │ │ │ └── index.test.js │ ├── reducers │ │ ├── route │ │ │ ├── index.test.js │ │ │ └── index.js │ │ ├── locale │ │ │ ├── index.test.js │ │ │ └── index.js │ │ └── error │ │ │ ├── index.test.js │ │ │ └── index.js │ ├── index.js │ ├── store.js │ └── hoc │ │ └── translate.js └── index.js ├── server.js ├── index.html ├── .babelrc ├── .eslintrc ├── webpack.config.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /src/app/views/index/style.sass: -------------------------------------------------------------------------------- 1 | .index-view 2 | -------------------------------------------------------------------------------- /src/app/index.sass: -------------------------------------------------------------------------------- 1 | html, body 2 | margin: 0 3 | padding: 0 4 | -------------------------------------------------------------------------------- /src/app/components/image-uploader/style.sass: -------------------------------------------------------------------------------- 1 | .image-uploader-component 2 | background: red 3 | -------------------------------------------------------------------------------- /src/app/history.js: -------------------------------------------------------------------------------- 1 | import { browserHistory } from 'react-router' 2 | 3 | export default browserHistory 4 | -------------------------------------------------------------------------------- /src/app/localization.json: -------------------------------------------------------------------------------- 1 | { 2 | "GREETING": { 3 | "FOR_YOU": { 4 | "en": "Hello World!" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | 3 | express().use(express.static('.')) 4 | .listen(process.env.PORT || 3000) 5 | -------------------------------------------------------------------------------- /src/app/utils/create-action-creator.js: -------------------------------------------------------------------------------- 1 | import { createAction as createActionCreator } from 'redux-actions' 2 | 3 | export default createActionCreator 4 | -------------------------------------------------------------------------------- /src/app/actions/error/index.js: -------------------------------------------------------------------------------- 1 | export const SET_ERROR = 'SET_ERROR' 2 | 3 | /** 4 | * Publish the error to the application's state. 5 | */ 6 | export const setError = (origin, payload) => 7 | ({ type: SET_ERROR, origin, payload }) 8 | -------------------------------------------------------------------------------- /src/app/reducers/route/index.test.js: -------------------------------------------------------------------------------- 1 | // import test from 'tape' 2 | // import reducer, { initialState } from 'app/reducers/route' 3 | // 4 | // // test('reducers/route', t => { 5 | // // t.deepEqual(reducer(undefined, { }), initialState) 6 | // // return t.end() 7 | // // }) 8 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Image Upload Demo 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/reducers/locale/index.test.js: -------------------------------------------------------------------------------- 1 | // import test from 'tape' 2 | // import reducer, { initialState } from 'app/reducers/locale' 3 | // // 4 | // // test('reducers/locale', t => { 5 | // // t.deepEqual(reducer(undefined, { }), initialState) 6 | // // return t.end() 7 | // // }) 8 | -------------------------------------------------------------------------------- /src/app/actions/route/index.js: -------------------------------------------------------------------------------- 1 | import createActionCreator from 'app/utils/create-action-creator' 2 | 3 | 4 | export const SET_ROUTE = 'SET_ROUTE' 5 | 6 | /** 7 | * Set the application's currently active route. 8 | */ 9 | export const setRoute = createActionCreator(SET_ROUTE) 10 | -------------------------------------------------------------------------------- /src/app/actions/locale/index.js: -------------------------------------------------------------------------------- 1 | import createActionCreator from 'app/utils/create-action-creator' 2 | 3 | 4 | export const SET_LOCALE = 'SET_LOCALE' 5 | 6 | /** 7 | * Set the application's currently active locale. 8 | */ 9 | export const setLocale = createActionCreator(SET_LOCALE) 10 | -------------------------------------------------------------------------------- /src/app/reducers/error/index.test.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import reducer, { initialState } from 'app/reducers/error' 3 | 4 | describe('reducers/error', () => { 5 | it('should initialize properly', () => { 6 | expect(reducer(undefined, { })).toEqual(initialState) 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /src/app/utils/create-reducer.js: -------------------------------------------------------------------------------- 1 | import recycle from 'redux-recycle' 2 | import { handleActions } from 'redux-actions' 3 | 4 | export default function createReducer(handlers, initialState, resetOn = []) { 5 | return recycle(handleActions(handlers, initialState), resetOn, initialState) 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom' 2 | 3 | const mount = document.getElementById('app') 4 | const render = app => ReactDOM.render(app, mount) 5 | 6 | if (module.hot) { 7 | module.hot.accept('app', () => render(require('app').default)) 8 | } 9 | 10 | render(require('app').default) 11 | -------------------------------------------------------------------------------- /src/app/reducers/error/index.js: -------------------------------------------------------------------------------- 1 | import createReducer from 'app/utils/create-reducer' 2 | import { SET_ERROR } from 'app/actions/error' 3 | 4 | const handlers = { 5 | [SET_ERROR]: (state, action) => action.payload, 6 | } 7 | 8 | export const initialState = null 9 | 10 | export default createReducer(handlers, initialState) 11 | -------------------------------------------------------------------------------- /src/app/reducers/locale/index.js: -------------------------------------------------------------------------------- 1 | import createReducer from 'app/utils/create-reducer' 2 | import { SET_LOCALE } from 'app/actions/locale' 3 | 4 | const handlers = { 5 | [SET_LOCALE]: (locale, action) => action.payload, 6 | } 7 | 8 | export const initialState = 'en' 9 | 10 | export default createReducer(handlers, initialState) 11 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | "stage-3" 6 | ], 7 | "env": { 8 | "development": { 9 | "plugins": [["react-transform", { 10 | "transforms": [{ 11 | "locals": ["module"], 12 | "imports": ["react"], 13 | "transform": "react-transform-hmr" 14 | }] 15 | }]] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/app/components/image-uploader/index.jsx: -------------------------------------------------------------------------------- 1 | import 'app/components/image-uploader/style.sass' 2 | 3 | import React, { PropTypes } from 'react' 4 | 5 | 6 | /** 7 | * 8 | */ 9 | export const ImageUploader = React.createClass({ 10 | render() { 11 | return ( 12 |
13 | ) 14 | }, 15 | }) 16 | 17 | export default ImageUploader 18 | -------------------------------------------------------------------------------- /src/app/reducers/route/index.js: -------------------------------------------------------------------------------- 1 | import createReducer from 'app/utils/create-reducer' 2 | import { SET_ROUTE } from 'app/actions/route' 3 | 4 | 5 | const handlers = { 6 | [SET_ROUTE]: (route, action) => action.payload, 7 | } 8 | 9 | export const initialState = { 10 | path: '/', 11 | actual: '/', 12 | 13 | query: { }, 14 | params: { }, 15 | 16 | component: null, 17 | } 18 | 19 | export default createReducer(handlers, initialState) 20 | -------------------------------------------------------------------------------- /src/app/actions/locale/index.test.js: -------------------------------------------------------------------------------- 1 | // import test from 'tape' 2 | // 3 | // import thunk from 'redux-thunk' 4 | // import createMockStore from 'redux-mock-store' 5 | // 6 | // import { 7 | // setLocale, 8 | // SET_LOCALE, 9 | // } from 'app/actions/locale' 10 | // 11 | // 12 | // const mockStore = createMockStore([thunk]) 13 | // 14 | // test('actions/locale#setLocale', t => { 15 | // const store = mockStore('en') 16 | // store.dispatch(setLocale('en')) 17 | // 18 | // t.deepEqual(store.getActions(), [ 19 | // { type: SET_LOCALE, payload: 'en' }, 20 | // ]) 21 | // return t.end() 22 | // }) 23 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | 6 | "extends": "airbnb", 7 | 8 | "rules": { 9 | "no-unused-vars": 0, 10 | "no-nested-ternary": 0, 11 | "react/sort-comp": 0, 12 | 13 | "semi": [ 2, "never" ], 14 | "react/prefer-es6-class": [ 2, "never" ], 15 | 16 | "key-spacing": [2, { 17 | "align": "value" 18 | }], 19 | 20 | "no-multi-spaces": [2, { 21 | "exceptions": { 22 | "ImportDeclaration": true, 23 | "VariableDeclarator": true, 24 | "AssignmentExpression": true 25 | } 26 | }] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/actions/route/index.test.js: -------------------------------------------------------------------------------- 1 | // import test from 'tape' 2 | // 3 | // import thunk from 'redux-thunk' 4 | // import createMockStore from 'redux-mock-store' 5 | // 6 | // import { 7 | // setRoute, 8 | // SET_ROUTE, 9 | // } from 'app/actions/route' 10 | // 11 | // 12 | // const mockStore = createMockStore([thunk]) 13 | // 14 | // test('actions/route#setRoute', t => { 15 | // const store = mockStore({ }) 16 | // store.dispatch(setRoute({ path: '/' })) 17 | // 18 | // t.deepEqual(store.getActions(), [ 19 | // { type: SET_ROUTE, payload: { path: '/' } } 20 | // ]) 21 | // return t.end() 22 | // }) 23 | -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | import 'app/index.sass' 2 | 3 | import React from 'react' 4 | import { Provider } from 'react-redux' 5 | import { Router, Route } from 'react-router' 6 | import { syncHistoryWithStore } from 'react-router-redux' 7 | 8 | import store from 'app/store' 9 | import history from 'app/history' 10 | 11 | const application = ( 12 | 13 | 14 | 15 | 16 | 17 | ) 18 | 19 | export default application 20 | -------------------------------------------------------------------------------- /src/app/actions/error/index.test.js: -------------------------------------------------------------------------------- 1 | // import test from 'tape' 2 | // 3 | // import thunk from 'redux-thunk' 4 | // import createMockStore from 'redux-mock-store' 5 | // 6 | // import { 7 | // setError, 8 | // SET_ERROR, 9 | // } from 'app/actions/error' 10 | // 11 | // 12 | // const mockStore = createMockStore([thunk]) 13 | // 14 | // test('actions/error#setError', t => { 15 | // const store = mockStore([]) 16 | // store.dispatch(setError('SAMPLE_ACTION', new Error('test'))) 17 | // 18 | // t.deepEqual(store.getActions(), [ 19 | // { type: SET_ERROR, origin: 'SAMPLE_ACTION', payload: new Error('test') }, 20 | // ]) 21 | // return t.end() 22 | // }) 23 | -------------------------------------------------------------------------------- /src/app/store.js: -------------------------------------------------------------------------------- 1 | import * as redux from 'redux' 2 | 3 | import thunk from 'redux-thunk' 4 | import logger from 'redux-logger' 5 | 6 | import * as router from 'react-router' 7 | import * as reduxRouter from 'react-router-redux' 8 | 9 | const createStore = 10 | redux.applyMiddleware( 11 | reduxRouter.routerMiddleware(router.browserHistory), thunk, logger())(redux.createStore) 12 | 13 | const rootReducer = redux.combineReducers({ 14 | /** 15 | * Vendor reducers. 16 | */ 17 | routing: reduxRouter.routerReducer, 18 | 19 | /** 20 | * Custom reducers. 21 | */ 22 | error: require('app/reducers/error').default, 23 | locale: require('app/reducers/locale').default, 24 | }) 25 | 26 | export default createStore(rootReducer) 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var EnvironmentPlugin = require('webpack').EnvironmentPlugin 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 6 | 7 | module.exports = { 8 | plugins: [ 9 | new EnvironmentPlugin(['NODE_ENV']), 10 | new ExtractTextPlugin('bundle.css', { disable: process.env.NODE_ENV !== 'production' }), 11 | ], 12 | resolve: { 13 | root: [ path.resolve('src'), path.resolve('node_modules') ], 14 | extensions: [ '', '.js', '.jsx' ], 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.sass$/, 20 | loader: ExtractTextPlugin.extract( 21 | 'style-loader', 'css-loader!postcss-loader!sass-loader?indentedSyntax=true'), 22 | }, 23 | ], 24 | }, 25 | postcss: function() { 26 | return [ require('autoprefixer') ] 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /src/app/hoc/translate.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import { connect } from 'react-redux' 3 | 4 | /** 5 | * Make sure given target is an object. 6 | */ 7 | const isObject = target => 8 | !Array.isArray(target) && 9 | !(target instanceof String) && 10 | !!target && typeof target === 'object' 11 | 12 | /** 13 | * Reduction for reading a property. 14 | */ 15 | const getDeepResource = (localization, key) => { 16 | if ((isObject(localization[key]) || typeof localization[key] === 'string')) { 17 | return localization[key] 18 | } 19 | return `Missing string resource for [${Object.keys.join(', ')}]` 20 | } 21 | 22 | /** 23 | * Wrap given component to provide a 'translate' context. 24 | */ 25 | export default function translate(component, resource = {}) { 26 | /** 27 | * Context-providing component to wrap the subcomponent. 28 | */ 29 | const Translator = React.createClass({ 30 | propTypes: { 31 | locale: PropTypes.string.isRequired, 32 | }, 33 | 34 | childContextTypes: { 35 | translate: PropTypes.func.isRequired, 36 | }, 37 | 38 | getChildContext() { 39 | return { 40 | translate: (...keys) => 41 | keys.concat([this.props.locale]).reduce(getDeepResource, resource), 42 | } 43 | }, 44 | 45 | render() { 46 | return React.createElement(component, this.props) 47 | }, 48 | }) 49 | 50 | const smart = connect( 51 | state => ({ 52 | locale: state.locale, 53 | })) 54 | 55 | return smart(Translator) 56 | } 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "img-resize-example", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "description": "Example of uploading and resizing images.", 6 | "scripts": { 7 | "dev": "kotatsu serve --port 3000 --babel --config webpack.config.js src/index.js", 8 | "build": "kotatsu build client --babel --config webpack.config.js --minify src/index.js -o dist", 9 | "start": "npm run build && node server.js" 10 | }, 11 | "dependencies": { 12 | "isomorphic-fetch": "2.2.1", 13 | "page": "1.6.4", 14 | "react": "0.14.7", 15 | "react-dom": "0.14.7", 16 | "react-redux": "4.4.1", 17 | "react-router": "2.0.1", 18 | "react-router-redux": "4.0.1", 19 | "redux": "3.3.1", 20 | "redux-actions": "0.9.1", 21 | "redux-logger": "2.6.1", 22 | "redux-mock-store": "1.0.2", 23 | "redux-recycle": "1.1.2", 24 | "redux-thunk": "2.0.1" 25 | }, 26 | "devDependencies": { 27 | "autoprefixer": "6.3.5", 28 | "babel-preset-es2015": "6.6.0", 29 | "babel-preset-react": "6.5.0", 30 | "babel-preset-react-hmre": "1.1.1", 31 | "babel-preset-stage-3": "6.5.0", 32 | "css-loader": "0.23.1", 33 | "eslint": "2.5.3", 34 | "eslint-config-airbnb": "6.2.0", 35 | "eslint-plugin-react": "4.2.3", 36 | "extract-text-webpack-plugin": "1.0.1", 37 | "glob": "7.0.3", 38 | "kotatsu": "0.13.0", 39 | "node-sass": "3.5.0-beta.1", 40 | "null-loader": "0.1.1", 41 | "postcss-loader": "0.8.2", 42 | "react-addons-test-utils": "0.14.7", 43 | "sass-loader": "3.2.0", 44 | "style-loader": "0.13.1", 45 | "tap-webpack-plugin": "1.1.0", 46 | "tape": "4.5.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/app/views/index/index.jsx: -------------------------------------------------------------------------------- 1 | import 'app/views/index/style.sass' 2 | 3 | import React, { PropTypes } from 'react' 4 | import { connect } from 'react-redux' 5 | 6 | import translate from 'app/hoc/translate' 7 | 8 | // simple checks for file type 9 | 10 | const isPDF = file => file && file.type === 'application/pdf' 11 | const isIMG = file => file && ( 12 | file.type === 'image/png' || 13 | file.type === 'image/jpeg' 14 | ) 15 | 16 | // make sure the images don't get too large while maintaining the original 17 | // aspect ratio 18 | 19 | const resize = ({ width, height }, { MAX_WIDTH, MAX_HEIGHT }) => { 20 | if(width > height) { 21 | if(width > MAX_WIDTH) { 22 | return { 23 | width: MAX_WIDTH, 24 | height: MAX_WIDTH / width * height, 25 | } 26 | } 27 | } 28 | if(height > MAX_HEIGHT) { 29 | return { 30 | width: MAX_HEIGHT / height * width, 31 | height: MAX_HEIGHT, 32 | } 33 | } 34 | return { width, height } 35 | } 36 | 37 | /** 38 | * Different declarations for the 39 | */ 40 | const A4 = { 41 | PPI72: { MAX_WIDTH: 595, MAX_HEIGHT: 842, DESC: '72 PPI' }, 42 | PPI200: { MAX_WIDTH: 1654, MAX_HEIGHT: 2339, DESC: '200 PPI' }, 43 | } 44 | 45 | /** 46 | * Index view of the application. 47 | */ 48 | export const IndexView = React.createClass({ 49 | propTypes: { 50 | route: PropTypes.object.isRequired, 51 | }, 52 | contextTypes: { 53 | translate: PropTypes.func.isRequired, 54 | }, 55 | getInitialState() { 56 | return { 57 | file: null, dataURL: '', size: A4.PPI72, 58 | } 59 | }, 60 | setSize(event) { 61 | this.setState({ size: A4[event.target.value] }, this.setImage) 62 | }, 63 | setFile(event) { 64 | event.preventDefault() 65 | return this.setState({ file: event.target.files[0] }, this.setImage) 66 | }, 67 | setImage() { 68 | const reader = new FileReader() 69 | 70 | reader.onload = () => { 71 | if (!isIMG(this.state.file)) return 72 | 73 | const img = document.createElement('img') 74 | const canvas = document.createElement('canvas') 75 | 76 | img.src = reader.result 77 | 78 | img.onload = () => { 79 | const { width, height } = resize(img, this.state.size) 80 | 81 | canvas.width = width 82 | canvas.height = height 83 | 84 | canvas.getContext('2d').drawImage(img, 0, 0, width, height) 85 | this.setState({ dataURL: canvas.toDataURL(this.state.file.type) }) 86 | } 87 | } 88 | return reader.readAsDataURL(this.state.file) 89 | }, 90 | render() { 91 | return ( 92 |
93 |
event.preventDefault()}> 94 | 95 | 99 |
100 |
101 | size ~ {atob(this.state.dataURL.substr(22)).length / 1000} kb 102 |
103 | {isIMG(this.state.file) && 104 | } 105 |
106 | ) 107 | }, 108 | }) 109 | 110 | const smart = connect( 111 | state => ({ 112 | route: state.routing, 113 | })) 114 | 115 | export default smart(translate(IndexView, require('app/localization.json'))) 116 | --------------------------------------------------------------------------------