├── .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 |
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 |
--------------------------------------------------------------------------------