├── .babelrc ├── .coveralls.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .scss-lint.yaml ├── .travis.yml ├── README.md ├── app ├── actions │ ├── auth.js │ ├── locale.js │ ├── page-title.js │ └── requests.js ├── beanManager │ ├── actions │ │ └── bean.js │ ├── components │ │ └── list.jsx │ ├── stores │ │ └── bean.js │ └── styles │ │ └── bean.scss ├── commentManager │ ├── actions │ │ └── comment.js │ ├── components │ │ └── list.jsx │ ├── stores │ │ └── comment.js │ └── styles │ │ └── comment.scss ├── components │ ├── app.jsx │ ├── footer.jsx │ ├── guides.jsx │ ├── header.jsx │ ├── protected.jsx │ └── shared │ │ ├── alloyeditor.jsx │ │ ├── dropImg.jsx │ │ ├── img.jsx │ │ ├── lang-picker.jsx │ │ ├── require-auth.jsx │ │ └── spinner.jsx ├── data │ ├── en.js │ └── fr.js ├── fonts │ ├── Roboto-Medium-webfont.eot │ ├── Roboto-Medium-webfont.svg │ ├── Roboto-Medium-webfont.ttf │ └── Roboto-Medium-webfont.woff ├── images │ ├── favicon.ico │ ├── no-preview.png │ ├── react-logo.png │ └── spinner.svg ├── index.js ├── main.js ├── pages │ ├── login-info.jsx │ ├── not-found.jsx │ └── server-error.jsx ├── postManager │ ├── actions │ │ └── posts.js │ ├── components │ │ ├── create.jsx │ │ ├── edit.jsx │ │ ├── list.jsx │ │ └── single.jsx │ ├── stores │ │ └── posts.js │ └── styles │ │ └── post.scss ├── routes.jsx ├── stores │ ├── auth.js │ ├── locale.js │ ├── page-title.js │ └── requests.js ├── styles │ ├── _application.scss │ ├── _fonts.scss │ ├── footer.scss │ ├── header.scss │ ├── lang-picker.scss │ ├── main.scss │ ├── mixins │ │ ├── _class-helper.scss │ │ ├── _hide-text.scss │ │ └── _reset-list.scss │ ├── spinner.scss │ └── test.scss ├── userManager │ ├── actions │ │ ├── resetpwd.js │ │ ├── role.js │ │ └── users.js │ ├── components │ │ ├── create.jsx │ │ ├── profile.jsx │ │ ├── resetpwd.jsx │ │ └── users.jsx │ ├── stores │ │ ├── resetpwd.js │ │ ├── role.js │ │ └── users.js │ └── styles │ │ ├── profile.scss │ │ └── users.scss └── utils │ ├── alt-resolver.js │ ├── flux.js │ ├── image-resolver.js │ ├── intl-loader.js │ ├── intl-polyfill.js │ ├── localized-routes.js │ └── promisify.js ├── karma.conf.js ├── package.json ├── processes.json ├── server ├── auth.js ├── bootstrap.js ├── config │ ├── all.json │ ├── development.js │ ├── init.js │ ├── production.js │ └── test.js ├── controllers │ ├── auth.js │ ├── bean.js │ ├── comment.js │ ├── index.js │ ├── post.js │ ├── role.js │ └── user.js ├── index.js ├── koa.js ├── models │ ├── bean.js │ ├── comment.js │ ├── index.js │ ├── post.js │ ├── role.js │ ├── tag.js │ └── user.js ├── router.jsx ├── services │ ├── index.js │ └── user.js └── views │ ├── layout.jade │ ├── login.jade │ └── main.jade ├── test ├── server │ ├── beforeAll.js │ ├── controllers │ │ ├── auth.spec.js │ │ ├── bean.spec.js │ │ ├── comment.spec.js │ │ ├── post.spec.js │ │ └── user.spec.js │ ├── mocha.opts │ └── resources │ │ └── mobious.png ├── spec │ ├── components │ │ ├── app.test.jsx │ │ ├── header.test.jsx │ │ ├── lang-picker.test.jsx │ │ ├── profile.test.jsx │ │ └── users.test.jsx │ ├── pages │ │ ├── not-found.test.jsx │ │ └── server-error.test.jsx │ ├── stores │ │ └── users.test.js │ └── utils │ │ ├── alt-resolver.test.js │ │ ├── image-resolver.test.js │ │ ├── intl-loader.test.js │ │ └── localized-routes.test.js └── utils │ ├── inject-lang.js │ └── stub-router-context.jsx ├── tests.webpack.js └── webpack ├── base.config.js ├── dev-server.js ├── dev.config.js ├── prod.config.js └── utils ├── clean-dist.js ├── start-koa.js └── write-stats.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } 4 | -------------------------------------------------------------------------------- /.coveralls.yml: -------------------------------------------------------------------------------- 1 | repo_token: 3eAArXSaxEd4JCV4030oyx4Ex7QhS5M3e 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | dist/ 4 | test/ 5 | app/ 6 | server/ 7 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint-config-airbnb", 4 | "plugins": ["react"], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "globals": { 11 | "chai": true 12 | }, 13 | "rules": { 14 | "react/jsx-uses-react": 1, 15 | "react/jsx-uses-vars": 1, 16 | "react/no-did-mount-set-state": 1, 17 | "react/no-did-update-set-state": 1, 18 | "react/no-multi-comp": 2, 19 | "react/prop-types": 2, 20 | "react/react-in-jsx-scope": 2, 21 | "react/self-closing-comp": 1, 22 | "react/wrap-multilines": 2, 23 | 24 | "quotes": [2, "single", "avoid-escape"], 25 | "comma-dangle": [2, "never"], 26 | "brace-style": [2, "stroustrup", {"allowSingleLine": true}], 27 | "no-underscore-dangle": 0, 28 | "space-in-brackets": 0, 29 | "func-names": 0, 30 | "no-else-return": 0, 31 | "no-param-reassign": 0, 32 | "no-reserved-keys": 0 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | text eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/webpack-stats.json 2 | coverage 3 | node_modules 4 | npm-debug.log 5 | dist 6 | .tmp 7 | .DS_Store 8 | .sass-cache 9 | .env 10 | .c9 11 | 12 | db.development.sqlite 13 | -------------------------------------------------------------------------------- /.scss-lint.yaml: -------------------------------------------------------------------------------- 1 | # Default application configuration that all configurations inherit from. 2 | 3 | scss_files: "**/*.scss" 4 | 5 | linters: 6 | BangFormat: 7 | enabled: true 8 | space_before_bang: true 9 | space_after_bang: false 10 | 11 | BorderZero: 12 | enabled: true 13 | convention: zero # or `none` 14 | 15 | ColorKeyword: 16 | enabled: true 17 | 18 | ColorVariable: 19 | enabled: true 20 | 21 | Comment: 22 | enabled: true 23 | 24 | DebugStatement: 25 | enabled: true 26 | 27 | DeclarationOrder: 28 | enabled: true 29 | 30 | DuplicateProperty: 31 | enabled: true 32 | 33 | ElsePlacement: 34 | enabled: true 35 | style: same_line # or 'new_line' 36 | 37 | EmptyLineBetweenBlocks: 38 | enabled: true 39 | ignore_single_line_blocks: true 40 | 41 | EmptyRule: 42 | enabled: true 43 | 44 | FinalNewline: 45 | enabled: true 46 | present: true 47 | 48 | HexLength: 49 | enabled: true 50 | style: short # or 'long' 51 | 52 | HexNotation: 53 | enabled: true 54 | style: uppercase # or 'lowercase' 55 | 56 | HexValidation: 57 | enabled: true 58 | 59 | IdSelector: 60 | enabled: false 61 | 62 | ImportantRule: 63 | enabled: true 64 | 65 | ImportPath: 66 | enabled: true 67 | leading_underscore: false 68 | filename_extension: false 69 | 70 | Indentation: 71 | enabled: true 72 | allow_non_nested_indentation: true 73 | character: space # or 'tab' 74 | width: 2 75 | 76 | LeadingZero: 77 | enabled: true 78 | style: exclude_zero # or 'include_zero' 79 | 80 | MergeableSelector: 81 | enabled: true 82 | force_nesting: true 83 | 84 | NameFormat: 85 | enabled: false 86 | allow_leading_underscore: true 87 | convention: BEM # or 'BEM', or a regex pattern 88 | 89 | NestingDepth: 90 | enabled: true 91 | max_depth: 3 92 | 93 | PlaceholderInExtend: 94 | enabled: true 95 | 96 | PropertyCount: 97 | enabled: false 98 | include_nested: false 99 | max_properties: 10 100 | 101 | PropertySortOrder: 102 | enabled: true 103 | order: ["position", "top", "right", "bottom", "left", "z-index", "display", "flex-direction", "flex-flow", "flex-wrap", "justify-content", "align-items", "align-content", "order", "flex", "flex-grow", "flex-shrink", "flex-basis", "align-self", "visibility", "overflow", "overflow-x", "overflow-y", "float", "clear", "table-layout", "border-collapse", "empty-cells", "box-sizing", "width", "min-width", "max-width", "height", "min-height", "max-height", "margin", "margin-top", "margin-right", "margin-bottom", "margin-left", "padding", "padding-top", "padding-right", "padding-bottom", "padding-left", "border", "border-width", "border-style", "border-color", "border-radius", "border-top", "border-top-width", "border-top-style", "border-top-color", "border-right", "border-right-width", "border-right-style", "border-right-color", "border-bottom", "border-bottom-width", "border-bottom-style", "border-bottom-color", "border-left", "border-left-width", "border-left-style", "border-left-color", "border-top-left-radius", "border-top-right-radius", "border-bottom-right-radius", "border-bottom-left-radius", "white-space", "content", "color", "background", "background-color", "background-image", "background-repeat", "background-attachment", "background-position", "background-size", "opacity", "font", "font-weight", "font-style", "font-variant", "font-size", "font-family", "letter-spacing", "line-height", "list-style", "list-style-type", "list-style-position", "list-style-image", "outline", "outline-width", "outline-style", "outline-color", "text-align", "text-decoration", "text-indent", "text-transform", "text-shadow", "animation", "transform", "transition", "box-shadow"] 104 | ignore_unspecified: true 105 | 106 | PropertySpelling: 107 | enabled: true 108 | extra_properties: [] 109 | 110 | QualifyingElement: 111 | enabled: false 112 | allow_element_with_attribute: false 113 | allow_element_with_class: false 114 | allow_element_with_id: false 115 | 116 | SelectorDepth: 117 | enabled: true 118 | max_depth: 3 119 | 120 | SelectorFormat: 121 | enabled: false 122 | convention: BEM # or 'BEM', or 'hyphenated_BEM', or 'snake_case', or 'camel_case', or a regex pattern 123 | 124 | Shorthand: 125 | enabled: true 126 | 127 | SingleLinePerProperty: 128 | enabled: true 129 | allow_single_line_rule_sets: true 130 | 131 | SingleLinePerSelector: 132 | enabled: true 133 | 134 | SpaceAfterComma: 135 | enabled: true 136 | 137 | SpaceAfterPropertyColon: 138 | enabled: true 139 | style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' 140 | 141 | SpaceAfterPropertyName: 142 | enabled: true 143 | 144 | SpaceBeforeBrace: 145 | enabled: true 146 | style: space # or 'new_line' 147 | allow_single_line_padding: false 148 | 149 | SpaceBetweenParens: 150 | enabled: true 151 | spaces: 0 152 | 153 | StringQuotes: 154 | enabled: true 155 | style: single_quotes # or double_quotes 156 | 157 | TrailingSemicolon: 158 | enabled: true 159 | 160 | TrailingZero: 161 | enabled: false 162 | 163 | UnnecessaryMantissa: 164 | enabled: true 165 | 166 | UnnecessaryParentReference: 167 | enabled: true 168 | 169 | UrlFormat: 170 | enabled: true 171 | 172 | UrlQuotes: 173 | enabled: true 174 | 175 | VariableForProperty: 176 | enabled: false 177 | properties: [] 178 | 179 | VendorPrefixes: 180 | enabled: false 181 | identifier_list: base 182 | additional_identifiers: [] 183 | excluded_identifiers: [] 184 | 185 | ZeroUnit: 186 | enabled: true 187 | 188 | Compass::*: 189 | enabled: false 190 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "iojs-v2.1.0" 4 | before_install: 5 | - "export DISPLAY=:99.0" 6 | - "sh -e /etc/init.d/xvfb start" 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mobious framework 2 | ================= 3 | 4 | base on [isomorphic-flux-boilerplate](https://github.com/iam4x/isomorphic-flux-boilerplate) 5 | 6 | - [sequelizejs - ORM](http://docs.sequelizejs.com/en/latest/) 7 | 8 | server side 9 | =========== 10 | 11 | bootstarp 12 | --------- 13 | 14 | create test data. 15 | 16 | define test data: server/bootstarp.js 17 | 18 | models 19 | ------ 20 | 21 | define models: server/models/XXX.js 22 | 23 | controllers 24 | ----------- 25 | 26 | define router: server/controllers.index.js 27 | 28 | run spec 29 | -------- 30 | 31 | command: `npm run rest-test` 32 | 33 | Isomorphic app module useage 34 | ============================ 35 | 36 | path 37 | ---- 38 | 39 | - app/#{module_name} 40 | 41 | ex: app/userManager 42 | 43 | Isomorphic app share resource 44 | ============================= 45 | 46 | app/pages 47 | --------- 48 | 49 | - 404: not find page 50 | - 500: server error page 51 | 52 | app/utils 53 | --------- 54 | 55 | use for general process 56 | 57 | > below information is Reference. 58 | 59 | ES6 Isomorphic Flux/ReactJS Boilerplate 60 | ======================================= 61 | 62 | [![Build Status](https://travis-ci.org/iam4x/isomorphic-flux-boilerplate.svg?branch=new-alt-resolver)](https://travis-ci.org/iam4x/isomorphic-flux-boilerplate)[![Coverage Status](https://coveralls.io/repos/iam4x/isomorphic-flux-boilerplate/badge.svg)](https://coveralls.io/r/iam4x/isomorphic-flux-boilerplate)[![Dependency Status](https://david-dm.org/iam4x/isomorphic-flux-boilerplate.svg)](https://david-dm.org/iam4x/isomorphic-flux-boilerplate)[![devDependency Status](https://david-dm.org/iam4x/isomorphic-flux-boilerplate/dev-status.svg)](https://david-dm.org/iam4x/isomorphic-flux-boilerplate#info=devDependencies)[![NPM Version](http://img.shields.io/npm/v/isomorphic-flux-boilerplate.svg?style=flat)](https://www.npmjs.com/package/isomorphic-flux-boilerplate) 63 | 64 | ES6 Isomorphic Flux/ReactJS Boilerplate 65 | ======================================= 66 | 67 | > A wonderfull boilerplate for **Flux/ReactJS** [isomorphic](http://nerds.airbnb.com/isomorphic-javascript-future-web-apps/) applications, running on **Koa**. 68 | 69 | **Demo:** http://isomorphic.iam4x.fr 70 | 71 | Libraries Included 72 | ------------------ 73 | 74 | - [react](https://facebook.github.io/react/) 75 | - [react-router](https://github.com/rackt/react-router) 76 | - [react-hot-loader](https://github.com/gaearon/react-hot-loader) 77 | - [react-intl](https://github.com/yahoo/react-intl) 78 | - [alt](https://github.com/goatslacker/alt) 79 | - [iso](https://github.com/goatslacker/iso) 80 | - [koa](http://koajs.com/) 81 | - [webpack](http://webpack.github.io/) 82 | - [babeljs](https://babeljs.io/) 83 | 84 | TL;DR 85 | ----- 86 | 87 | Use with `iojs^1.8.0` or `nodejs^0.12.0`, clone the repo, `npm install` and `npm run dev`. 88 | 89 | Learn React ([react-prime-draft](https://github.com/mikechau/react-primer-draft)), learn Flux and Alt ([alt guide](http://alt.js.org/guide/)). 90 | 91 | Wrap you async actions into promises, send them to `altResolver` with `altResolver.resolve(xxx)` for async server side rendering (see [app/actions/users.js:31](https://github.com/iam4x/isomorphic-flux-boilerplate/blob/master/app/actions/users.js#L31)). 92 | 93 | Build for production with `npm run build`, don't forget to run the tests before `npm test`. 94 | 95 | Concepts 96 | -------- 97 | 98 | **Koa** will be our server for the server side rendering, we use **alt** for our Flux architecture and **react-router** for routing in our app. 99 | 100 | With **iso** as helper we can populate **alt** flux stores before the first rendering and have a complete async isomorphic React application. 101 | 102 | Run this boilerplate, you will see the server is fetching some fake users and will populate the `UserStore` with this data. **Koa** will render the first markup, serve the JavaScript and then it will entirely run on the client. 103 | 104 | Flux 105 | ---- 106 | 107 | We use [alt](http://alt.js.org) instance as [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html) implementation. 108 | 109 | We need to use instances for isomorphic applications, to have a unique store/actions per requests on the server. 110 | 111 | On the client, Flux is initialized in `app/main.js` and sent to our first React Component via props (`this.props.flux`). Everytime you want to uses stores or actions in a component you need to give it access through props. 112 | 113 | On the server, it's similar but Flux is initialized in `server/router.jsx`. The instance is sent to `alt-resolver` for rendering components with the correct props. 114 | 115 | Learn more about [alt instances](http://alt.js.org/docs/altInstances) in the alt documentation. 116 | 117 | Internationalization (i18n) 118 | --------------------------- 119 | 120 | We use [react-intl](https://github.com/yahoo/react-intl) for internationalization, it uses browser implementation of [Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl). For older browser and for node, we load the polyfill. 121 | 122 | - Support localized strings (see [data/en.js](https://github.com/iam4x/isomorphic-flux-boilerplate/blob/master/app%2Fdata%2Fen.js)\) 123 | - Support localized dates, times and currencies. 124 | 125 | Lang files and Intl polyfill are compiled into webpack chunks, for lazy-loading depending the locale of the user. 126 | 127 | If user changes locale, it is saved into a cookie `_lang` and used by the server to know the locale of rendering. If there's no `_lang` cookie, server will rely on `Accept-Language` request header. Server will set `` on rendering. 128 | 129 | Thank's to [gpbl/react-locale-hot-switch](https://github.com/gpbl/react-locale-hot-switch) for the implementation example! 130 | 131 | Async data-fetching 132 | ------------------- 133 | 134 | Alt-resolver is the magic thing about the boilerplate, it will be our tool for resolving promises (data-fetching) before server side rendering. 135 | 136 | Wrap data-fetching requests from actions into promises and send them to `altResolver` like: 137 | 138 | ``` 139 | fetch() { 140 | const promise = (resolve) => { 141 | request 142 | .get('http://example.com/api/users') 143 | .end((response) => { 144 | // fire new action to send data to store 145 | this.actions.fetchSuccess(response.body); 146 | return resolve(); 147 | }); 148 | }; 149 | // Send the `promise` to altResolver 150 | this.alt.resolve(promise); 151 | } 152 | ``` 153 | 154 | Call the fetch action from component in the `componentWillMount` method: 155 | 156 | ``` 157 | static propTypes: { 158 | flux: React.PropTypes.object.isRequired 159 | } 160 | 161 | componentWillMount() { 162 | const usersActions = this.props.flux.getActions('users'); 163 | return usersActions.fetch(); 164 | } 165 | ``` 166 | 167 | On browser side, the rendering won't be stopped and will resolve the promise instantly. 168 | 169 | On server side, `altResolver.render` will fire a first render to collect all the promises needed for a complete rendering. It will then resolve them, and try to re-render the application for a complete markup. 170 | 171 | Open `app/actions/users.js`, `app/utils/alt-resolver.js`, `app/stores/users.js` for more information about data-fetching. 172 | 173 | How to `require()` images on server side 174 | ---------------------------------------- 175 | 176 | On client with webpack, you can directly `require()` images for your images DOM element like: 177 | 178 | ``` 179 | 180 | ``` 181 | 182 | Webpack will load them through the `url-loader` and if it's too big it will sent through `file-loader` for minification/compilation. The results is an image with a new filename for cache busting. 183 | 184 | But on node, `require()` an image will just throw an exception. There's an util for loading image on server side to achieve this: 185 | 186 | ``` 187 | import imageResolver from 'utils/image-resolver' 188 | 189 | let image; 190 | // On browser just require() the image as usual 191 | if (process.env.BROWSER) { 192 | image = require('images/logo.png'); 193 | } 194 | else { 195 | image = imageResolver('images/logo.png'); 196 | } 197 | 198 | ... 199 | render () { 200 | return ( 201 | 202 | ); 203 | } 204 | ... 205 | ``` 206 | 207 | The utils/image-resolver with match the original image name with the compiled one. 208 | 209 | Voilà! You can `require()` images on server side too. 210 | 211 | Installation / How-to 212 | --------------------- 213 | 214 | I recommend to use [io.js](https://iojs.org/) to take advantages of `ES6` without `--harmony` flag on `NodeJS`. 215 | 216 | It's super easy to do with [nvm](https://github.com/creationix/nvm): 217 | 218 | - `$ nvm install iojs` 219 | - `$ nvm use iojs` 220 | - `$ nvm alias default iojs` (to make `node` default to `iojs`\) 221 | 222 | But it works well with `nodejs^0.12.0` as well :) 223 | 224 | After that, you will just need to clone the repo and install dependancies: 225 | 226 | - `$ git clone -o upstream https://github.com/iam4x/isomorphic-flux-boilerplate.git app` 227 | - `$ cd app && npm install` 228 | 229 | (Don't forget to add your remote origin: `$ git remote origin git@github.com:xxx/xxx.git`\) 230 | 231 | ### Run the project in development: 232 | 233 | - `$ npm run dev` 234 | 235 | Open your browser to `http://localhost:3002` and you will see the magic happens! Try to disable JavaScript in your browser, you will still be able to navigate between pages of the application. Enjoy the power of isomorphic applications! 236 | 237 | (Note: ports 3000-3002 are needed, you can change this with `$ PORT=3050 npm run dev` it will run on 3050-3052) 238 | 239 | ### Run tests 240 | 241 | - `$ npm test` will run the tests once 242 | - `$ ./node_modules/.bin/karma start` will watch for changes and run the tests on change 243 | 244 | ### Build project: 245 | 246 | Just run `$ npm run build`, it will produce these tasks: 247 | 248 | - Run tests from `test/spec/**/*.jsx` 249 | - Concat & minify styles to `/dist/app-[hash].css` 250 | - Concat & minify scripts to `/dist/js/app-[hash].js` 251 | 252 | ### Update the boilerplate 253 | 254 | You can fetch the upstream branch and merge it into your master: 255 | 256 | - `$ git checkout master` 257 | - `$ git fetch upstream` 258 | - `$ git merge upstream/master` 259 | - `$ npm install` 260 | 261 | ### Run in production 262 | 263 | Build the project first: 264 | 265 | - `$ npm run build` 266 | 267 | Then start the koa server: 268 | 269 | - `$ NODE_ENV=production node server/index.js` (iojs) 270 | - `$ NODE_ENV=production node --harmony server/index.js` (nodejs 0.12.x) 271 | 272 | You can also use `processes.json` to run the application with [PM2 Monitor](https://github.com/Unitech/pm2) on your production server (customize it for your use): 273 | 274 | - `$ pm2 start processes.json` 275 | 276 | ### Learn more 277 | 278 | - [Official ReactJS website](http://facebook.github.io/react/) 279 | - [Official ReactJS wiki](https://github.com/facebook/react/wiki) 280 | - [Official Flux website](http://facebook.github.io/flux/) 281 | - [ReactJS Conf 2015 links](https://gist.github.com/yannickcr/148110d3ca658ad96c2b) 282 | - [Learn ES6](https://babeljs.io/docs/learn-es6/) 283 | - [ES6 Features](https://github.com/lukehoban/es6features#readme) 284 | 285 | ### Common errors 286 | 287 | - SASS compilation hang when importing same file more than once (see [#62](https://github.com/iam4x/isomorphic-flux-boilerplate/issues/62)\) 288 | -------------------------------------------------------------------------------- /app/actions/auth.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class AuthActions { 5 | constructor() { 6 | this.generateActions('localLoginSuccess', 'fetchStatusSuccess', 'localLoginFail'); 7 | } 8 | 9 | localLogin(params) { 10 | const promise = (resolve) => { 11 | this.alt.getActions('requests').start(); 12 | request.post(`${baseUrl}auth/login`) 13 | .send(params) 14 | .end((error, res) => { 15 | if (error) { 16 | if (error.status === 401) { 17 | this.actions.localLoginFail(res.body); 18 | this.alt.getActions('requests').success(); 19 | return resolve(); 20 | } 21 | return resolve(error); 22 | } 23 | this.actions.localLoginSuccess(res.body); 24 | this.alt.getActions('requests').success(); 25 | return resolve(); 26 | }); 27 | }; 28 | this.alt.resolve(promise); 29 | } 30 | 31 | fetchStatus() { 32 | const promise = (resolve) => { 33 | this.alt.getActions('requests').start(); 34 | request.get(`${baseUrl}auth/status`) 35 | .end((error, res) => { 36 | if (error) return resolve(error); 37 | this.actions.fetchStatusSuccess(res.body); 38 | this.alt.getActions('requests').success(); 39 | return resolve(res.body); 40 | }); 41 | }; 42 | 43 | this.alt.resolve(promise); 44 | } 45 | } 46 | 47 | export default AuthActions; 48 | -------------------------------------------------------------------------------- /app/actions/locale.js: -------------------------------------------------------------------------------- 1 | import intlLoader from 'utils/intl-loader'; 2 | 3 | class LocaleActions { 4 | constructor() { 5 | this.generateActions('switchLocaleSuccess'); 6 | } 7 | 8 | async switchLocale(locale) { 9 | if (locale) { 10 | const {messages} = await intlLoader(locale); 11 | return this.actions.switchLocaleSuccess({locale, messages}); 12 | } 13 | } 14 | } 15 | 16 | export default LocaleActions; 17 | -------------------------------------------------------------------------------- /app/actions/page-title.js: -------------------------------------------------------------------------------- 1 | class PageTitleActions { 2 | constructor() { 3 | this.generateActions('set'); 4 | } 5 | } 6 | 7 | export default PageTitleActions; 8 | -------------------------------------------------------------------------------- /app/actions/requests.js: -------------------------------------------------------------------------------- 1 | class RequestsActions { 2 | constructor() { 3 | this.generateActions('start', 'success', 'fail'); 4 | } 5 | } 6 | 7 | export default RequestsActions; 8 | -------------------------------------------------------------------------------- /app/beanManager/actions/bean.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class BeanActions { 5 | constructor() { 6 | this.generateActions( 7 | 'fetchSuccess' 8 | ); 9 | } 10 | 11 | fetch() { 12 | const promise: Function = (resolve) => { 13 | let that = this; 14 | that.alt.getActions('requests').start(); 15 | 16 | request.get(baseUrl + 'rest/bean') 17 | .end((error, res) => { 18 | if (error) return resolve(error); 19 | that.actions.fetchSuccess(res.body.beans); 20 | that.alt.getActions('requests').success(); 21 | return resolve(); 22 | }); 23 | }; 24 | this.alt.resolve(promise); 25 | } 26 | } 27 | 28 | export default BeanActions; 29 | -------------------------------------------------------------------------------- /app/beanManager/components/list.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import ListenerMixin from 'alt/mixins/ListenerMixin'; 4 | import {IntlMixin} from 'react-intl'; 5 | 6 | if (process.env.BROWSER) { 7 | require('beanManager/styles/bean.scss'); 8 | } 9 | 10 | export default React.createClass({ 11 | mixins: [ListenerMixin, IntlMixin], 12 | contextTypes: { 13 | router: React.PropTypes.func 14 | }, 15 | propTypes: { 16 | flux: React.PropTypes.object.isRequired 17 | }, 18 | getInitialState() { 19 | return this.props.flux.getStore('bean').getState(); 20 | }, 21 | componentWillMount() { 22 | return this.props.flux.getActions('bean').fetch(); 23 | }, 24 | componentDidMount() { 25 | this.listenTo(this.props.flux.getStore('bean'), this.handleStoreChange); 26 | }, 27 | handleStoreChange() { 28 | this.setState(this.getInitialState()); 29 | }, 30 | 31 | renderBeans() { 32 | return this.state.beans.map((bean, index) => { 33 | return ( 34 | 35 | {bean.name} 36 | 37 | ); 38 | }); 39 | }, 40 | render() { 41 | return ( 42 |
43 |

{this.getIntlMessage('beanManager.title')}

44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | {this.renderBeans()} 52 | 53 |
{this.getIntlMessage('beanManager.name')}
54 |
55 | ); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /app/beanManager/stores/bean.js: -------------------------------------------------------------------------------- 1 | class BeanStore { 2 | 3 | constructor() { 4 | this.bindActions(this.alt.getActions('bean')); 5 | this.beans = []; 6 | } 7 | 8 | onFetchSuccess(beans) { 9 | return this.setState({beans}); 10 | } 11 | } 12 | 13 | export default BeanStore; 14 | -------------------------------------------------------------------------------- /app/beanManager/styles/bean.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | 3 | .app--users { 4 | margin: 0 auto; 5 | 6 | td, 7 | th { 8 | padding: 5px 10px; 9 | } 10 | 11 | thead { 12 | border-bottom: 1px solid $black; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/commentManager/actions/comment.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class CommentActions { 5 | constructor() { 6 | this.generateActions( 7 | 'fetchSuccess', 'createSuccess', 'fetchOneSuccess' 8 | ); 9 | } 10 | create(params) { 11 | const promise: Function = (resolve) => { 12 | // fake xhr 13 | this.alt.getActions('requests').start(); 14 | 15 | request.post(baseUrl + 'rest/comment/') 16 | .send(params) 17 | .end((error, res) => { 18 | if (error) return resolve(error); 19 | 20 | let createdComment = res.body.comment; 21 | this.actions.createSuccess(createdComment); 22 | this.alt.getActions('requests').success(); 23 | return resolve(); 24 | }, 300); 25 | }; 26 | this.alt.resolve(promise); 27 | } 28 | 29 | fetch() { 30 | const promise: Function = (resolve) => { 31 | let that = this; 32 | that.alt.getActions('requests').start(); 33 | 34 | request.get(baseUrl + 'rest/comment/') 35 | // .set('Accept', 'application/json') 36 | .end((error, res) => { 37 | if (error) return resolve(error); 38 | that.actions.fetchSuccess(res.body.comments); 39 | that.alt.getActions('requests').success(); 40 | return resolve(); 41 | }); 42 | }; 43 | this.alt.resolve(promise); 44 | } 45 | 46 | fetchOne(id: string) { 47 | const promise = (resolve) => { 48 | this.alt.getActions('requests').start(); 49 | request.get(baseUrl + 'rest/comment/' + `${id}`) 50 | .end((error, res) => { 51 | if (error) return resolve(error); 52 | const comment: Object = res.body.comment; 53 | this.actions.fetchOneSuccess(comment); 54 | this.alt.getActions('requests').success(); 55 | return resolve(); 56 | }); 57 | }; 58 | 59 | this.alt.resolve(promise); 60 | } 61 | 62 | 63 | } 64 | 65 | export default CommentActions; 66 | -------------------------------------------------------------------------------- /app/commentManager/components/list.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {IntlMixin} from 'react-intl'; 3 | import {Button, Panel, Col, Input} from 'react-bootstrap'; 4 | 5 | if (process.env.BROWSER) { 6 | require('commentManager/styles/comment.scss'); 7 | } 8 | 9 | class CommentList extends Component { 10 | 11 | static propTypes = { 12 | flux: PropTypes.object.isRequired 13 | } 14 | 15 | _getIntlMessage = IntlMixin.getIntlMessage 16 | 17 | state = { 18 | comments: this.props.flux 19 | .getStore('comment') 20 | .getState().comments 21 | }; 22 | 23 | componentWillMount() { 24 | return this.props.flux.getActions('comment').fetch(); 25 | } 26 | 27 | componentDidMount() { 28 | this.props.flux 29 | .getStore('comment') 30 | .listen(this._handleStoreChange); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.props.flux 35 | .getStore('comment') 36 | .unlisten(this._handleStoreChange); 37 | } 38 | 39 | _handleStoreChange = (state) => { 40 | return this.setState(state); 41 | } 42 | 43 | _handleSubmit(event) { 44 | event.preventDefault(); 45 | 46 | let newComment = { 47 | author: React.findDOMNode(this.refs.c_author.refs.input).value.trim(), 48 | content: React.findDOMNode(this.refs.c_content.refs.input).value.trim() 49 | }; 50 | 51 | this.props.flux.getActions('comment').create(newComment); 52 | React.findDOMNode(this.refs.c_author.refs.input).value = ''; 53 | React.findDOMNode(this.refs.c_content.refs.input).value = ''; 54 | } 55 | 56 | renderComments() { 57 | return this.state.comments.map((comment, index) => { 58 | return ( 59 |
60 | 61 | {comment.content} 62 | 63 |
64 | ); 65 | }); 66 | } 67 | 68 | renderCommentInput() { 69 | return ( 70 |
71 | 72 | 73 |
74 | 75 | 76 | 77 |
78 | 79 |
80 |
81 | ); 82 | } 83 | 84 | render() { 85 | return ( 86 | 87 |
88 |

{this._getIntlMessage('commentManager.title')}

89 |
90 | {this.renderComments()} 91 |
92 |
93 |
94 | {this.renderCommentInput()} 95 |
96 | 97 | ); 98 | } 99 | } 100 | 101 | export default CommentList; 102 | -------------------------------------------------------------------------------- /app/commentManager/stores/comment.js: -------------------------------------------------------------------------------- 1 | class CommentStore { 2 | 3 | constructor() { 4 | this.bindActions(this.alt.getActions('comment')); 5 | this.comments = []; 6 | } 7 | 8 | onFetchSuccess(comments) { 9 | return this.setState({comments}); 10 | } 11 | 12 | onCreateSuccess(comment) { 13 | const comments: Array = this.comments.slice(); 14 | comments.push(comment); 15 | return this.setState({comments}); 16 | } 17 | 18 | // static getBySeed(id) { 19 | // const comment: Array = this.getState().comment; 20 | // let singleComment = this.getState().comment; 21 | // if (singleComment === null) singleComment = comment.find((comment) => comment.id.toString() === id.toString()); 22 | // return {comment: singleComment}; 23 | // } 24 | // 25 | // onFetchOneSuccess(comment) { 26 | // return this.setState({comment}); 27 | // } 28 | 29 | 30 | } 31 | 32 | export default CommentStore; 33 | -------------------------------------------------------------------------------- /app/commentManager/styles/comment.scss: -------------------------------------------------------------------------------- 1 | $black: #000; 2 | 3 | .app--users { 4 | margin: 0 auto; 5 | 6 | td, 7 | th { 8 | padding: 5px 10px; 9 | } 10 | 11 | thead { 12 | border-bottom: 1px solid $black; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/components/app.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react/addons'; 2 | 3 | import Header from 'components/header'; 4 | import Footer from 'components/footer'; 5 | import {Grid, Row} from 'react-bootstrap'; 6 | 7 | 8 | if (process.env.BROWSER) { 9 | require('styles/main.scss'); 10 | } 11 | 12 | class App extends Component { 13 | 14 | static propTypes = { 15 | flux: PropTypes.object.isRequired, 16 | children: PropTypes.element 17 | } 18 | 19 | constructor(props, context) { 20 | super(props, context); 21 | 22 | this.state = { 23 | i18n: props.flux 24 | .getStore('locale') 25 | .getState() 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | this.props.flux 31 | .getStore('locale') 32 | .listen(this._handleLocaleChange); 33 | 34 | this.props.flux 35 | .getStore('page-title') 36 | .listen(this._handlePageTitleChange); 37 | } 38 | 39 | componentWillUnmount() { 40 | this.props.flux 41 | .getStore('locale') 42 | .unlisten(this._handleLocaleChange); 43 | 44 | this.props.flux 45 | .getStore('page-title') 46 | .unlisten(this._handlePageTitleChange); 47 | } 48 | 49 | _handleLocaleChange = (i18n) => { 50 | return this.setState({i18n}); 51 | } 52 | 53 | _handlePageTitleChange({title}) { 54 | document.title = title; 55 | } 56 | 57 | // If we have children components sent by `react-router` 58 | // we need to clone them and add them the correct 59 | // locale and messages sent from the Locale Store 60 | renderChild = (child) => { 61 | return React.addons.cloneWithProps(child, {...this.state.i18n}); 62 | } 63 | 64 | render() { 65 | global.flux = this.props.flux; 66 | return ( 67 |
68 |
69 |
70 | 71 | 72 | {React.Children.map(this.props.children, this.renderChild)} 73 | 74 | 75 |
76 |
77 |
78 | ); 79 | } 80 | 81 | } 82 | 83 | export default App; 84 | -------------------------------------------------------------------------------- /app/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | if (process.env.BROWSER) { 4 | require('styles/footer.scss'); 5 | } 6 | 7 | class Footer extends Component { 8 | 9 | render() { 10 | return ( 11 |
12 |
13 |
14 | StarFork'}}/> 15 |
16 |
17 | ); 18 | } 19 | 20 | } 21 | 22 | export default Footer; 23 | -------------------------------------------------------------------------------- /app/components/guides.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {IntlMixin} from 'react-intl'; 3 | import {Panel, Col} from 'react-bootstrap'; 4 | 5 | class Guides extends Component { 6 | 7 | static propTypes = { 8 | flux: PropTypes.object.isRequired 9 | } 10 | 11 | _getIntlMessage = IntlMixin.getIntlMessage 12 | 13 | componentWillMount() { 14 | this.props.flux 15 | .getActions('page-title') 16 | .set(this._getIntlMessage('guides.page-title')); 17 | } 18 | 19 | render() { 20 | return ( 21 | 22 | Guides}> 23 |

Coming soon...

24 |
25 | 26 | ); 27 | } 28 | 29 | } 30 | 31 | export default Guides; 32 | -------------------------------------------------------------------------------- /app/components/header.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, {Component, PropTypes} from 'react'; 3 | import {Link} from 'react-router'; 4 | import {IntlMixin} from 'react-intl'; 5 | 6 | import imageResolver from 'utils/image-resolver'; 7 | import Spinner from 'components/shared/spinner'; 8 | import LangPicker from 'components/shared/lang-picker'; 9 | import Img from 'components/shared/img'; 10 | 11 | import {Navbar, Nav, NavItem, Glyphicon} from 'react-bootstrap'; 12 | // Load styles for the header 13 | // and load the `react-logo.png` image 14 | // for the `` element 15 | let reactLogo; 16 | if (process.env.BROWSER) { 17 | require('styles/header.scss'); 18 | reactLogo = require('images/react-logo.png'); 19 | } 20 | else { 21 | reactLogo = imageResolver('images/react-logo.png'); 22 | } 23 | 24 | class Header extends Component { 25 | 26 | static propTypes: { 27 | flux: PropTypes.object.isRequired, 28 | locales: PropTypes.array.isRequired 29 | } 30 | 31 | _getIntlMessage = IntlMixin.getIntlMessage 32 | 33 | state = { 34 | spinner: false, 35 | authStatus: this.props.flux 36 | .getStore('auth') 37 | .getState().authStatus 38 | } 39 | 40 | componentDidMount() { 41 | this.props.flux 42 | .getStore('requests') 43 | .listen(this._handleRequestStoreChange); 44 | this.props.flux 45 | .getStore('auth') 46 | .listen(this._handleAuthStoreChange); 47 | } 48 | 49 | _handleRequestStoreChange = ({inProgress}) => { 50 | return this.setState({spinner: inProgress}); 51 | } 52 | 53 | _handleAuthStoreChange = ({authStatus}) => { 54 | return this.setState({authStatus: authStatus}); 55 | } 56 | renderLogin() { 57 | if (this.state.authStatus.isAuthenticated) { 58 | return ( 59 | {this.state.authStatus.sessionUser.username}  60 | 61 | 62 | ); 63 | } 64 | else { 65 | return ( 66 |
  • 67 | 68 | 69 | {this._getIntlMessage('loginMessage.login')} 70 | 71 |
  • 72 | ); 73 | } 74 | } 75 | 76 | render() { 77 | const {locales, flux} = this.props; 78 | const [activeLocale] = locales; 79 | return ( 80 |
    81 | 82 | react-logo 83 | mobious 84 | }> 85 | 112 | 115 | 118 | 119 | 120 |
    121 | ); 122 | } 123 | } 124 | 125 | export default Header; 126 | -------------------------------------------------------------------------------- /app/components/protected.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {IntlMixin} from 'react-intl'; 3 | import {Panel, Col} from 'react-bootstrap'; 4 | import requireAuth from 'components/shared/require-auth'; 5 | 6 | const Protected = requireAuth('admin', class Protected extends Component { 7 | 8 | static propTypes = { 9 | flux: PropTypes.object.isRequired 10 | } 11 | 12 | _getIntlMessage = IntlMixin.getIntlMessage 13 | 14 | componentWillMount() { 15 | this.props.flux 16 | .getActions('page-title') 17 | .set(this._getIntlMessage('protected.page-title')); 18 | 19 | this.state = this.props.flux.getStore('auth').getState(); 20 | } 21 | 22 | render() { 23 | return ( 24 | 25 | Protected}> 26 |

    secret mesaage

    27 | {() => { 28 | if (this.state !== undefined) { 29 | return ( 30 |
    31 |

    {`login user's email is ${this.state.authStatus.sessionUser.email}`}

    32 |

    {`login user's authority is ${this.state.authStatus.authority}`}

    33 |
    34 | ); 35 | } 36 | }()} 37 |
    38 | 39 | ); 40 | } 41 | 42 | }); 43 | 44 | export default Protected; 45 | -------------------------------------------------------------------------------- /app/components/shared/alloyeditor.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | 3 | if (process.env.BROWSER) { 4 | window.React = React; 5 | window.CKEDITOR_BASEPATH = '/alloyeditor/'; 6 | require('alloyeditor/dist/alloy-editor/alloy-editor-all.js'); 7 | require('alloyeditor/dist/alloy-editor/assets/alloy-editor-ocean.css'); 8 | } 9 | 10 | class Alloyeditor extends Component { 11 | static propTypes = { 12 | content: PropTypes.string 13 | } 14 | 15 | componentDidMount() { 16 | setTimeout(this._initAlloyEditor, 500); 17 | } 18 | 19 | _initAlloyEditor = () => { 20 | window.AlloyEditor.editable('AlloyeditorContent', { 21 | uiNode: 'uiNode'}); 22 | } 23 | 24 | render() { 25 | let contentSytle = { 26 | 'minHeight': '5px', 27 | border: '1px solid #ccc', 28 | 'borderRadius': '4px' 29 | }; 30 | let body = null; 31 | if (this.props.label !== undefined) { 32 | body = ( 33 |
    34 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 | ); 41 | } 42 | else { 43 | body = ( 44 |
    45 |
    46 |
    47 |
    48 | ); 49 | } 50 | return body; 51 | } 52 | 53 | } 54 | export default Alloyeditor; 55 | -------------------------------------------------------------------------------- /app/components/shared/dropImg.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import Dropzone from 'react-dropzone'; 3 | import {Col} from 'react-bootstrap'; 4 | 5 | let nonImage; 6 | 7 | if (process.env.BROWSER) { 8 | nonImage = require('images/no-preview.png'); 9 | } 10 | 11 | class DropImg extends Component { 12 | 13 | static propTypes = { 14 | apiUrl: PropTypes.string.isRequired, 15 | flux: PropTypes.object.isRequired, 16 | preview: PropTypes.string 17 | } 18 | 19 | onDrop(files) { 20 | this.props.flux.getActions('posts').uploadImg(this.props.apiUrl, files, this.props.preview); 21 | } 22 | 23 | render() { 24 | let img = ''; 25 | if (this.props.preview === null || this.props.preview === '' || this.props.preview === undefined ) { 26 | img = nonImage; 27 | } 28 | else { 29 | img = '/assets/images/post/' + this.props.preview; 30 | } 31 | return ( 32 | 33 | 34 | 35 |
    點擊或拖拉圖片至此區塊以上載圖片
    36 |
    37 | 38 | ); 39 | } 40 | 41 | } 42 | 43 | export default DropImg; 44 | -------------------------------------------------------------------------------- /app/components/shared/img.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | 5 | class Img extends Component { 6 | static propTypes = { 7 | circle: PropTypes.bool, 8 | rounded: PropTypes.bool, 9 | responsive: PropTypes.bool, 10 | height: PropTypes.number, 11 | width: PropTypes.number 12 | } 13 | 14 | static defaultProps = { 15 | height: null, 16 | width: null 17 | } 18 | 19 | render() { 20 | let style = {}; 21 | let classes = classNames({ 22 | 'img-circle': this.props.circle, 23 | 'img-rounded': this.props.rounded, 24 | 'img-responsive': this.props.responsive 25 | }); 26 | if (this.props.height) { 27 | style.height = this.props.height; 28 | delete this.props.height; 29 | } 30 | if (this.props.width) { 31 | style.width = this.props.width; 32 | delete this.props.width; 33 | } 34 | 35 | let props = Object.assign({ 36 | style: style, 37 | className: classes 38 | }, this.props); 39 | 40 | return ( 41 | 42 | ); 43 | } 44 | } 45 | 46 | export default Img; 47 | -------------------------------------------------------------------------------- /app/components/shared/lang-picker.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | if (process.env.BROWSER) { 5 | require('styles/lang-picker.scss'); 6 | } 7 | 8 | class LangPicker extends Component { 9 | 10 | static propTypes = { 11 | activeLocale: PropTypes.string.isRequired, 12 | onChange: PropTypes.func.isRequired 13 | } 14 | 15 | locales = ['fr', 'en'] 16 | 17 | renderLocale = ::this.renderLocale 18 | renderLocale(locale, index) { 19 | return ( 20 |
  • 21 | 24 | {locale} 25 | 26 |
  • 27 | ); 28 | } 29 | 30 | render() { 31 | return ( 32 | 38 | ); 39 | } 40 | } 41 | 42 | export default LangPicker; 43 | -------------------------------------------------------------------------------- /app/components/shared/require-auth.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {baseUrl} from '../../../server/config/init'; 3 | import request from 'superagent'; 4 | 5 | export default function requireAuth(role, ChildComponent) { 6 | class Authenticated extends Component { 7 | 8 | static async onEnter(next, transition, callback) { 9 | let flux = global.flux; 10 | let authStatus = {}; 11 | if (flux) { 12 | authStatus = flux.getStore('auth').getState().authStatus; 13 | } 14 | else { 15 | authStatus = await new Promise((resolve, reject) => { 16 | request.get(`${baseUrl}auth/status`) 17 | .end((error, res) => { 18 | if (error) return reject(error); 19 | return resolve(res.body); 20 | }); 21 | }); 22 | } 23 | 24 | if (!authStatus.isAuthenticated) { 25 | transition.to('/login-info'); 26 | return callback(); 27 | } 28 | 29 | if (!role) return callback(); 30 | 31 | if (authStatus.authority !== role) { 32 | transition.to('/login-info'); 33 | return callback(); 34 | } 35 | return callback(); 36 | } 37 | 38 | render() { 39 | return ; 40 | } 41 | } 42 | 43 | return Authenticated; 44 | } 45 | -------------------------------------------------------------------------------- /app/components/shared/spinner.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | if (process.env.BROWSER) { 5 | require('styles/spinner.scss'); 6 | } 7 | 8 | class Spinner extends Component { 9 | 10 | static propTypes = { 11 | active: PropTypes.bool 12 | } 13 | 14 | render() { 15 | return
    ; 16 | } 17 | } 18 | 19 | export default Spinner; 20 | -------------------------------------------------------------------------------- /app/data/en.js: -------------------------------------------------------------------------------- 1 | export default { 2 | messages: { 3 | header: { 4 | users: 'Users List', 5 | guides: 'Guides', 6 | protected: 'Protected', 7 | posts: 'Post Manager' 8 | }, 9 | guides: { 10 | 'page-title': 'Guides' 11 | }, 12 | protected: { 13 | 'page-title': 'Protected Page' 14 | }, 15 | profile: { 16 | 'page-title': 'Profile - {fullName}', 17 | 'not-found-page-title': 'User profile not found' 18 | }, 19 | userManager: { 20 | 'page-title': 'Users', 21 | title: 'Some random users', 22 | email: 'Email address', 23 | actions: 'Actions', 24 | add: 'Add User', 25 | profile: 'Profile', 26 | resetPassword: { 27 | title: 'Reset Password', 28 | password: 'Password', 29 | password_again: 'Password again', 30 | close: 'Close', 31 | reset: 'Reset', 32 | resetting: 'Resetting...', 33 | success: 'Success. Password is reset.' 34 | } 35 | }, 36 | beanManager: { 37 | title: 'Bean Manager', 38 | name: 'bean name' 39 | }, 40 | postManager: { 41 | 'page-title': 'Posts', 42 | title: 'Post Manager', 43 | name: 'Post Name', 44 | add: 'Add Post', 45 | edit: 'Edit', 46 | delete: 'Delete', 47 | editor: 'Editor', 48 | content: 'Post Content' 49 | }, 50 | post: { 51 | 'page-title': 'Post', 52 | label_title: '文章標題', 53 | label_content: '文章內文', 54 | label_tag: '文章標籤', 55 | label_tagPlaceHolder: '加入標籤' 56 | }, 57 | post_create: { 58 | title: '新增文章', 59 | submit: '新增' 60 | }, 61 | post_edit: { 62 | title: '修改文章', 63 | submit: '修改' 64 | }, 65 | commentManager: { 66 | title: '留言版' 67 | }, 68 | loginMessage: { 69 | login: 'Login', 70 | logout: 'Logout', 71 | success: { 72 | title: 'Login Success.', 73 | depiction: 'Enjoy It. :)' 74 | }, 75 | fail: { 76 | title: 'Fail.', 77 | depiction: 'Please Check Your Account or Password.' 78 | }, 79 | activated: { 80 | title: 'Not Login.', 81 | depiction: 'Your Account is Not Activated yet. Please Contact Your System Administrator.' 82 | } 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /app/data/fr.js: -------------------------------------------------------------------------------- 1 | export default { 2 | messages: { 3 | header: { 4 | users: 'Utilisateurs', 5 | guides: 'Guides', 6 | protected: 'Privé', 7 | posts: 'Post Manager' 8 | }, 9 | guides: { 10 | 'page-title': 'Guides' 11 | }, 12 | protected: { 13 | 'page-title': 'Page protégée' 14 | }, 15 | profile: { 16 | 'page-title': 'Profil - {fullName}', 17 | 'not-found-page-title': 'User profile not found' 18 | }, 19 | userManager: { 20 | 'page-title': 'Utilisateurs', 21 | title: 'Des utilisateurs au hasard', 22 | email: 'Adresse email', 23 | actions: 'Opérations', 24 | add: 'Ajouter un utilisateur', 25 | profile: 'Profil', 26 | resetPassword: { 27 | title: 'Réinitialiser le mot de passe', 28 | password: 'Mot de passe', 29 | password_again: 'Mot de passe à nouveau', 30 | close: 'Fermer', 31 | reset: 'Reste', 32 | resetting: 'Réinitialisation...', 33 | success: 'Succès. Mot de passe est réinitialisé.' 34 | } 35 | }, 36 | beanManager: { 37 | title: 'Bean Manager', 38 | name: 'bean name' 39 | }, 40 | postManager: { 41 | 'page-title': 'Posts', 42 | title: 'Post Manager', 43 | name: 'post name', 44 | add: 'Ajouter un poste', 45 | edit: 'Edit', 46 | delete: 'Delete', 47 | editor: 'Editor', 48 | content: 'post content' 49 | }, 50 | post: { 51 | 'page-title': 'Post', 52 | label_title: 'Title', 53 | label_content: 'Content', 54 | label_tag: 'Tag', 55 | label_tagPlaceHolder: 'Add Tag' 56 | }, 57 | post_create: { 58 | title: 'Create Post', 59 | submit: 'create' 60 | }, 61 | post_edit: { 62 | title: 'Update Post', 63 | submit: 'update' 64 | }, 65 | commentManager: { 66 | title: '留言版' 67 | }, 68 | loginMessage: { 69 | login: 'Login', 70 | logout: 'Logout', 71 | success: { 72 | title: 'Login Success.', 73 | depiction: 'Enjoy It. :)' 74 | }, 75 | fail: { 76 | title: 'Fail.', 77 | depiction: 'Please Check Your Account or Password.' 78 | }, 79 | activated: { 80 | title: 'Not Login.', 81 | depiction: 'Your Account is Not Activated yet. Please Contact Your System Administrator.' 82 | } 83 | } 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /app/fonts/Roboto-Medium-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/app/fonts/Roboto-Medium-webfont.eot -------------------------------------------------------------------------------- /app/fonts/Roboto-Medium-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/app/fonts/Roboto-Medium-webfont.ttf -------------------------------------------------------------------------------- /app/fonts/Roboto-Medium-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/app/fonts/Roboto-Medium-webfont.woff -------------------------------------------------------------------------------- /app/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/app/images/favicon.ico -------------------------------------------------------------------------------- /app/images/no-preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/app/images/no-preview.png -------------------------------------------------------------------------------- /app/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/app/images/react-logo.png -------------------------------------------------------------------------------- /app/images/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 19 | 20 | 21 | 28 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | // Browser ES6 Polyfill 2 | require('babel/polyfill'); 3 | 4 | // Start application 5 | require('./main'); 6 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | import Iso from 'iso'; 2 | import React from 'react'; 3 | import Router from 'react-router'; 4 | import BrowserHistory from 'react-router/lib/BrowserHistory'; 5 | 6 | // Paths are relative to `app` directory 7 | import Flux from 'utils/flux'; 8 | import intlLoader from 'utils/intl-loader'; 9 | 10 | if (process.env.NODE_ENV === 'development') { 11 | require('debug').enable('dev,koa'); 12 | } 13 | 14 | const boostrap = () => { 15 | return new Promise((resolve) => { 16 | Iso.bootstrap((initialState, __, container) => { 17 | resolve({initialState, __, container}); 18 | }); 19 | }); 20 | }; 21 | 22 | (async () => { 23 | // Init alt instance 24 | const flux = new Flux(); 25 | 26 | // bootstrap application with data from server 27 | const boot = await boostrap(); 28 | flux.bootstrap(boot.initialState); 29 | 30 | // load the intl-polyfill if needed 31 | // load the correct data/{lang}.json into app 32 | const locale = flux.getStore('locale').getLocale(); 33 | const {messages} = await intlLoader(locale); 34 | flux.getActions('locale').switchLocaleSuccess({locale, messages}); 35 | 36 | // load routes after int-polyfill 37 | // routes.jsx imports components using the `window.Intl` 38 | // it should be defined before 39 | const routerProps = { 40 | routes: require('routes'), 41 | history: new BrowserHistory(), 42 | createElement: (component, props) => { 43 | // Take locale and messages from `locale` store 44 | // and pass them to every components rendered from `Router` 45 | const i18n = flux.getStore('locale').getState(); 46 | return React.createElement( 47 | component, 48 | Object.assign(props, {flux, ...i18n}) 49 | ); 50 | } 51 | }; 52 | 53 | // Render `` in the same container as the SSR 54 | React.render( 55 | React.createElement(Router, {...routerProps}), 56 | boot.container 57 | ); 58 | 59 | // Tell `alt-resolver` we have done the first render 60 | // next promises will be resolved 61 | flux._resolver._firstClientSideRender = false; 62 | })(); 63 | -------------------------------------------------------------------------------- /app/pages/login-info.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {IntlMixin} from 'react-intl'; 3 | import {baseUrl} from '../../server/config/init'; 4 | import {Button, Panel, Col, Input, Alert} from 'react-bootstrap'; 5 | 6 | 7 | class LoginInfo extends Component { 8 | 9 | static contextTypes = { 10 | router: PropTypes.func 11 | } 12 | 13 | static propTypes = { 14 | flux: React.PropTypes.object.isRequired, 15 | locales: PropTypes.array.isRequired, 16 | params: React.PropTypes.object.isRequired 17 | } 18 | 19 | _getIntlMessage = IntlMixin.getIntlMessage 20 | 21 | state = { 22 | authStatus: this.props.flux 23 | .getStore('auth') 24 | .getState().authStatus 25 | }; 26 | 27 | componentWillMount() { 28 | this.props.flux 29 | .getActions('page-title') 30 | .set(this._getIntlMessage('protected.page-title')); 31 | 32 | this.state.authStatus = this.props.flux.getStore('auth').getState().authStatus; 33 | this.state.loginMessage = this.props.flux.getStore('auth').getState().loginMessage; 34 | } 35 | 36 | componentDidMount() { 37 | this.props.flux 38 | .getStore('auth') 39 | .listen(this._handleStoreChange); 40 | } 41 | 42 | componentWillUnmount() { 43 | this.props.flux 44 | .getStore('auth') 45 | .unlisten(this._handleStoreChange); 46 | } 47 | 48 | _handleStoreChange = (state) => { 49 | return this.setState(state); 50 | } 51 | 52 | handleSubmit(event) { 53 | event.preventDefault(); 54 | 55 | let loginUser = { 56 | username: this.refs.username.refs.input.getDOMNode().value, 57 | password: this.refs.password.refs.input.getDOMNode().value 58 | }; 59 | 60 | this.props.flux.getActions('auth').localLogin(loginUser); 61 | // this.context.router.transitionTo('users'); 62 | } 63 | renderLoginForm() { 64 | let redirectUri = baseUrl + decodeURIComponent(this.props.params.nextPath); 65 | let url = `/auth/facebook?redirect_uri=${redirectUri}`; 66 | if (!this.state.authStatus.isAuthenticated) { 67 | return ( 68 | 69 |
    70 | 71 | 72 | 73 | 74 | 75 | 76 |
    77 |
    78 | ); 79 | } 80 | } 81 | 82 | renderLoginMessage() { 83 | if (this.state.loginMessage !== '') { 84 | let alertStyle = this.state.loginMessage === 'success' ? 'success' : 'danger'; 85 | return ( 86 | 87 | {this._getIntlMessage(`loginMessage.${this.state.loginMessage}.title`)} 88 |

    {this._getIntlMessage(`loginMessage.${this.state.loginMessage}.depiction`)}

    89 |
    90 | ); 91 | } 92 | } 93 | 94 | render() { 95 | return ( 96 | 97 | {this.renderLoginMessage()} 98 | {this.renderLoginForm()} 99 | 100 | ); 101 | } 102 | } 103 | export default LoginInfo; 104 | -------------------------------------------------------------------------------- /app/pages/not-found.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class NotFound extends Component { 4 | 5 | render() { 6 | return (

    404

    ); 7 | } 8 | 9 | } 10 | 11 | export default NotFound; 12 | -------------------------------------------------------------------------------- /app/pages/server-error.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class ServerError extends Component { 4 | 5 | render() { 6 | return (

    500

    ); 7 | } 8 | 9 | } 10 | 11 | export default ServerError; 12 | -------------------------------------------------------------------------------- /app/postManager/actions/posts.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class PostsActions { 5 | constructor() { 6 | this.generateActions( 7 | 'fetchSuccess', 'createSuccess', 'fetchOneSuccess', 'updateSuccess', 'updateImgSuccess', 'updateEditorSuccess', 'removeSuccess' 8 | ); 9 | } 10 | create(params) { 11 | const promise: Function = (resolve) => { 12 | // fake xhr 13 | this.alt.getActions('requests').start(); 14 | 15 | request.post(baseUrl + 'rest/post/') 16 | .send(params) 17 | .end((error, res) => { 18 | if (error) return resolve(error); 19 | let createdPost = res.body.post; 20 | this.actions.updateSuccess(createdPost); 21 | this.alt.getActions('requests').success(); 22 | return resolve(); 23 | }, 300); 24 | }; 25 | this.alt.resolve(promise); 26 | } 27 | 28 | update(id:string, params) { 29 | const promise = (resolve) => { 30 | let that = this; 31 | that.alt.getActions('requests').start(); 32 | request.put(baseUrl + 'rest/post/' + `${id}`) 33 | .send(params) 34 | .end((error, res) => { 35 | if (error) return resolve(error); 36 | let editPost = res.body.post; 37 | this.actions.updateSuccess(editPost); 38 | this.alt.getActions('requests').success(); 39 | return resolve(); 40 | }, 300); 41 | }; 42 | this.alt.resolve(promise); 43 | } 44 | 45 | updateEditor(id:string, params) { 46 | const promise = (resolve) => { 47 | let that = this; 48 | that.alt.getActions('requests').start(); 49 | request.put(baseUrl + 'rest/post/updateEditor/' + `${id}`) 50 | .send(params) 51 | .end((error, res) => { 52 | if (error) return resolve(error); 53 | this.actions.updateEditorSuccess(res.body.post); 54 | this.alt.getActions('requests').success(); 55 | return resolve(); 56 | }, 300); 57 | }; 58 | this.alt.resolve(promise); 59 | } 60 | 61 | fetch() { 62 | const promise: Function = (resolve) => { 63 | let that = this; 64 | that.alt.getActions('requests').start(); 65 | 66 | request.get(baseUrl + 'rest/post') 67 | // .set('Accept', 'application/json') 68 | .end((error, res) => { 69 | if (error) return resolve(error); 70 | that.actions.fetchSuccess(res.body.posts); 71 | that.alt.getActions('requests').success(); 72 | return resolve(); 73 | }); 74 | }; 75 | this.alt.resolve(promise); 76 | } 77 | 78 | fetchOne(id: string) { 79 | const promise = (resolve) => { 80 | this.alt.getActions('requests').start(); 81 | request.get(baseUrl + 'rest/post/' + `${id}`) 82 | .end((error, res) => { 83 | if (error) return resolve(error); 84 | let tagNames = res.body.post.Tags.map((tag => { 85 | return tag.name; 86 | })); 87 | res.body.post.Tags = tagNames.sort(); 88 | this.actions.fetchOneSuccess(res.body.post); 89 | this.alt.getActions('requests').success(); 90 | return resolve(); 91 | }); 92 | }; 93 | 94 | this.alt.resolve(promise); 95 | } 96 | 97 | uploadImg(url: string, files: Array, uploadFileName: string) { 98 | const promise = (resolve) => { 99 | this.alt.getActions('requests').start(); 100 | files.forEach((file) => { 101 | request.post(url) 102 | .attach('file', file) 103 | .end((err, res) => { 104 | if (err) return resolve(err); 105 | let resObj = res.body; 106 | uploadFileName = resObj.filename; 107 | this.actions.updateImgSuccess(uploadFileName); 108 | this.alt.getActions('requests').success(); 109 | }); 110 | }); 111 | 112 | return resolve(); 113 | }; 114 | 115 | this.alt.resolve(promise); 116 | } 117 | 118 | remove(id: number) { 119 | const promise = (resolve) => { 120 | let that = this; 121 | that.alt.getActions('requests').start(); 122 | 123 | request.del(`${baseUrl}rest/post/${id}`) 124 | .end(() => { 125 | that.actions.removeSuccess(id); 126 | that.alt.getActions('requests').success(); 127 | return resolve(); 128 | }); 129 | }; 130 | this.alt.resolve(promise); 131 | } 132 | 133 | 134 | } 135 | 136 | export default PostsActions; 137 | -------------------------------------------------------------------------------- /app/postManager/components/create.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {Button, Input, Col, Panel} from 'react-bootstrap'; 3 | import {IntlMixin} from 'react-intl'; 4 | import Alloyeditor from 'components/shared/alloyeditor'; 5 | import TagsInput from 'react-tagsinput'; 6 | import DropImg from 'components/shared/dropImg'; 7 | 8 | if (process.env.BROWSER) { 9 | require('react-tagsinput/react-tagsinput.css'); 10 | } 11 | 12 | class Create extends Component { 13 | 14 | _getIntlMessage = IntlMixin.getIntlMessage 15 | 16 | static contextTypes = { 17 | router: PropTypes.func 18 | } 19 | 20 | static propTypes: { 21 | flux: React.PropTypes.object.isRequired 22 | } 23 | 24 | state = { 25 | img: this.props.flux. 26 | getStore('posts').img 27 | } 28 | 29 | componentWillMount() { 30 | this.props.flux 31 | .getActions('posts'); 32 | } 33 | 34 | componentDidMount() { 35 | this.props.flux 36 | .getStore('posts') 37 | .listen(this._handleStoreChange); 38 | } 39 | 40 | _handleStoreChange = (state) => { 41 | this.setState(state); 42 | } 43 | 44 | _handleSubmit = (event) => { 45 | event.preventDefault(); 46 | 47 | let newPost = { 48 | title: React.findDOMNode(this.refs.title.refs.input).value, 49 | content: React.findDOMNode(this.refs.content.refs.content).innerHTML, 50 | img: this.state.img, 51 | tags: this.refs.tags.getTags() 52 | }; 53 | 54 | this.props.flux.getActions('posts').create(newPost); 55 | this.context.router.transitionTo('/postList'); 56 | } 57 | 58 | 59 | render() { 60 | let body = null; 61 | 62 | body = ( 63 |
    64 | 65 |
    66 | {this._getIntlMessage('post_create.title')}}> 68 | 69 | 70 |
    71 | 72 |
    73 | 74 |
    75 |
    76 | 77 |
    78 | 79 | 80 | 81 |
    82 | 83 |
    84 | ); 85 | 86 | 87 | return body; 88 | } 89 | 90 | } 91 | 92 | export default Create; 93 | -------------------------------------------------------------------------------- /app/postManager/components/edit.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {Button, Input, Col, Panel} from 'react-bootstrap'; 3 | import {IntlMixin} from 'react-intl'; 4 | import Alloyeditor from 'components/shared/alloyeditor'; 5 | import TagsInput from 'react-tagsinput'; 6 | import DropImg from 'components/shared/dropImg'; 7 | 8 | if (process.env.BROWSER) { 9 | require('react-tagsinput/react-tagsinput.css'); 10 | } 11 | 12 | class Edit extends Component { 13 | 14 | _getIntlMessage = IntlMixin.getIntlMessage 15 | 16 | static contextTypes = { 17 | router: PropTypes.func 18 | } 19 | 20 | static propTypes: { 21 | flux: React.PropTypes.object.isRequired 22 | } 23 | 24 | state = { 25 | post: this.props.flux 26 | .getStore('posts') 27 | .getBySeed(this.props.params.id).post, 28 | img: this.props.flux 29 | .getStore('posts').img 30 | }; 31 | 32 | componentWillMount() { 33 | this.props.flux 34 | .getActions('posts') 35 | .fetchOne(this.props.params.id); 36 | } 37 | 38 | componentDidMount() { 39 | this.props.flux 40 | .getStore('posts') 41 | .listen(this._handleStoreChange); 42 | } 43 | 44 | _handleStoreChange = (state) => { 45 | this.setState(state); 46 | } 47 | 48 | _handleSubmit = (event) => { 49 | event.preventDefault(); 50 | 51 | let newPost = { 52 | id: React.findDOMNode(this.refs.id).value, 53 | title: React.findDOMNode(this.refs.title.refs.input).value, 54 | content: React.findDOMNode(this.refs.content.refs.content).innerHTML, 55 | img: this.state.img, 56 | tags: this.refs.tags.getTags() 57 | }; 58 | 59 | this.props.flux.getActions('posts').update(this.props.params.id, newPost); 60 | this.context.router.transitionTo('/postList'); 61 | } 62 | 63 | _handleTitle = (event) => { 64 | let state = this.state; 65 | state.post.title = event.target.value; 66 | this.setState(state); 67 | } 68 | 69 | _handleTags = (event) => { 70 | let state = this.state; 71 | state.post.Tags = event; 72 | this.setState(state); 73 | } 74 | render() { 75 | let body = null; 76 | if (this.state.post !== undefined) { 77 | body = ( 78 |
    79 | 80 |
    81 | {this._getIntlMessage('post_edit.title')}}> 83 | 84 | 85 | 86 |
    87 | 88 |
    89 | 90 |
    91 |
    92 | 93 |
    94 | 95 | 96 | 97 |
    98 | 99 |
    100 | ); 101 | } 102 | else { 103 | body = ( 104 |
    105 |

    106 |
    107 | ); 108 | } 109 | 110 | return body; 111 | } 112 | 113 | } 114 | 115 | export default Edit; 116 | -------------------------------------------------------------------------------- /app/postManager/components/list.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {Link} from 'react-router'; 3 | import {IntlMixin} from 'react-intl'; 4 | import {Table, Panel, Col, Glyphicon, Input, Button} from 'react-bootstrap'; 5 | 6 | if (process.env.BROWSER) { 7 | require('postManager/styles/post.scss'); 8 | } 9 | 10 | class Posts extends Component { 11 | 12 | static propTypes = { 13 | flux: PropTypes.object.isRequired 14 | } 15 | 16 | _getIntlMessage = IntlMixin.getIntlMessage 17 | 18 | state = { 19 | posts: this.props.flux 20 | .getStore('posts') 21 | .getState().posts, 22 | authStatus: this.props.flux 23 | .getStore('auth') 24 | .getState().authStatus, 25 | editors: this.props.flux 26 | .getStore('role') 27 | .getState().attributes 28 | }; 29 | 30 | componentWillMount() { 31 | this.props.flux 32 | .getActions('page-title') 33 | .set(this._getIntlMessage('postManager.page-title')); 34 | this.props.flux 35 | .getActions('posts') 36 | .fetch(); 37 | this.props.flux. 38 | getActions('role').fetchByAttributes('editor'); 39 | } 40 | 41 | componentDidMount() { 42 | this.props.flux 43 | .getStore('posts') 44 | .listen(this._handleStoreChange); 45 | this.props.flux 46 | .getStore('role') 47 | .listen(this._handleStoreChange); 48 | } 49 | 50 | componentWillUnmount() { 51 | this.props.flux 52 | .getStore('posts') 53 | .unlisten(this._handleStoreChange); 54 | this.props.flux 55 | .getStore('role') 56 | .unlisten(this._handleStoreChange); 57 | } 58 | 59 | _handleStoreChange = (state) => { 60 | state.authStatus = this.props.flux.getStore('auth').getState().authStatus; 61 | state.editors = this.props.flux.getStore('role').getState().attributes; 62 | return this.setState(state); 63 | } 64 | 65 | renderPost = (post, index) => { 66 | return ( 67 | 68 | 69 | {post.id} 70 | 71 | 72 | 73 | {post.title} 74 | 75 | 76 | {this.renderEdit(post)} 77 | {this.renderEditor(post.EditorId, post.id)} 78 | 79 | ); 80 | } 81 | renderEditor(EditorId, postId) { 82 | if (this.state.authStatus.authority === 'admin') { 83 | return ( 84 | 85 | 86 | 87 | {this.state.editors.map(this.renderEditorsOptions)} 88 | 89 | 90 | ); 91 | } 92 | } 93 | renderEditorsOptions = (editor, index) => { 94 | return ( 95 | 96 | ); 97 | } 98 | updateEditor = (postId, event) => { 99 | this.props.flux.getActions('posts').updateEditor(postId, {editorId: event.target.value}); 100 | } 101 | renderEdit(post) { 102 | if (this.state.authStatus.authority === 'editor') { 103 | let userId = this.state.authStatus.sessionUser.id; 104 | if (userId === post.CreatorId || userId === post.EditorId) { 105 | return ( 106 |
    107 | 108 | 109 | 110 | 111 | 112 | 113 | 117 | 118 |
    119 | ); 120 | } else return (
    ); 121 | } 122 | } 123 | _removePost(id) { 124 | this.props.flux 125 | .getActions('posts') 126 | .remove(id); 127 | } 128 | renderTh(isEditorOrCreator, message) { 129 | if (isEditorOrCreator) { 130 | return ( 131 | 132 | {message} 133 | 134 | ); 135 | } 136 | } 137 | 138 | renderCreateBtn(isEditorOrCreator) { 139 | if (isEditorOrCreator) { 140 | return ( 141 | 142 | 143 | 146 | 147 | 148 | ); 149 | } 150 | } 151 | render() { 152 | let isEditorOrCreator = false; 153 | let userId = this.state.authStatus.authority === '' ? 0 : this.state.authStatus.sessionUser.id; 154 | this.state.posts.forEach((post) => { 155 | if (post.CreatorId === userId || post.EditorId === userId) { 156 | isEditorOrCreator = true; 157 | } 158 | }); 159 | 160 | 161 | return ( 162 | 163 | {this._getIntlMessage('postManager.title')}}> 165 | 166 | 167 | 168 | 169 | 172 | {this.renderTh(isEditorOrCreator, this._getIntlMessage('postManager.edit'))} 173 | {this.renderTh(isEditorOrCreator, this._getIntlMessage('postManager.delete'))} 174 | {() => { 175 | if (this.state.authStatus.authority === 'admin') { 176 | return ( 177 | 180 | ); 181 | } 182 | }()} 183 | 184 | 185 | 186 | {this.state.posts.map(this.renderPost)} 187 | 188 |
    Post ID 170 | {this._getIntlMessage('postManager.name')} 171 | 178 | {this._getIntlMessage('postManager.editor')} 179 |
    189 |
    190 | {this.renderCreateBtn(isEditorOrCreator)} 191 | 192 | ); 193 | } 194 | } 195 | 196 | export default Posts; 197 | -------------------------------------------------------------------------------- /app/postManager/components/single.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {IntlMixin} from 'react-intl'; 3 | import {Label} from 'react-bootstrap'; 4 | 5 | class Single extends Component { 6 | 7 | static propTypes = { 8 | flux: PropTypes.object.isRequired, 9 | params: PropTypes.object.isRequired 10 | } 11 | 12 | _getIntlMessage = IntlMixin.getIntlMessage 13 | 14 | 15 | state = this.props.flux 16 | .getStore('posts') 17 | .getBySeed(this.props.params.id) 18 | 19 | componentWillMount() { 20 | return this.props.flux.getActions('posts').fetchOne(this.props.params.id); 21 | } 22 | 23 | 24 | componentDidMount() { 25 | this.props.flux 26 | .getStore('posts') 27 | .listen(this._handleStoreChange); 28 | } 29 | 30 | 31 | _handleStoreChange = (state) => { 32 | this.setState(state); 33 | } 34 | renderTags = (tag) => { 35 | return ( 36 | 37 |   38 | 39 | ); 40 | } 41 | render() { 42 | return ( 43 |
    44 | 45 | {() => { 46 | if (this.state.post !== undefined) { 47 | return ( 48 |
    49 |

    50 | {this.state.post.title} 51 |

    52 | {this.state.post.Tags.map(this.renderTags)} 53 |
    54 |
    55 | 56 |
    57 | ); 58 | } 59 | }()} 60 |
    61 | ); 62 | } 63 | } 64 | 65 | export default Single; 66 | -------------------------------------------------------------------------------- /app/postManager/stores/posts.js: -------------------------------------------------------------------------------- 1 | class PostStore { 2 | 3 | constructor() { 4 | this.bindActions(this.alt.getActions('posts')); 5 | this.posts = []; 6 | this.post = { Tags: []}; 7 | this.img = ''; 8 | } 9 | 10 | onCreateSuccess(post) { 11 | const posts: Array = this.posts.slice(); 12 | posts.push(post); 13 | return this.setState({posts}); 14 | } 15 | 16 | onUpdateSuccess(post) { 17 | return this.setState({post}); 18 | } 19 | 20 | onFetchSuccess(posts) { 21 | return this.setState({posts}); 22 | } 23 | 24 | static getBySeed(id) { 25 | const posts: Array = this.getState().posts; 26 | let singlePost = this.getState().post; 27 | if (singlePost === null) singlePost = posts.find((post) => post.id.toString() === id.toString()); 28 | return {post: singlePost}; 29 | } 30 | 31 | onFetchOneSuccess(post) { 32 | return this.setState({post: post, img: post.img}); 33 | } 34 | 35 | updateImgSuccess(img) { 36 | return this.setState({img}); 37 | } 38 | 39 | onUpdateEditorSuccess(newPost) { 40 | const posts: Array = this.posts; 41 | posts.forEach((post, index) => { 42 | if (post.id === newPost.id) { 43 | this.posts[index] = newPost; 44 | // UI select default value. 45 | if (newPost.EditorId === null) this.posts[index].EditorId = 0; 46 | } 47 | }); 48 | 49 | return this.setState({posts}); 50 | } 51 | 52 | onRemoveSuccess(id) { 53 | const posts: Array = this.posts.slice(); 54 | let removeIndex = -1; 55 | posts.forEach((post, index) => { 56 | if (post.id === id) removeIndex = index; 57 | }); 58 | 59 | if (removeIndex >= 0) posts.splice(removeIndex, 1); 60 | 61 | return this.setState({posts}); 62 | } 63 | 64 | } 65 | 66 | export default PostStore; 67 | -------------------------------------------------------------------------------- /app/postManager/styles/post.scss: -------------------------------------------------------------------------------- 1 | .app-posts { 2 | .panel-footer{ 3 | text-align: center; 4 | } 5 | .table{ 6 | margin-bottom: 0px !important; 7 | tr{ 8 | td,th { 9 | padding: 5px 10px; 10 | vertical-align: middle !important; 11 | 12 | &:first-child{ 13 | width: 20%; 14 | text-align: center; 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/routes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route} from 'react-router'; 3 | import {generateRoute} from 'utils/localized-routes'; 4 | // import requireAuth from 'components/shared/require-auth'; 5 | // let protectedPage = require('./components/protected'); 6 | // const decorated = requireAuth('admin', protectedPage); 7 | 8 | export default ( 9 | 10 | {generateRoute({ 11 | paths: ['/', '/users'], 12 | component: require('./userManager/components/users') 13 | })} 14 | {generateRoute({ 15 | paths: ['/guides'], 16 | component: require('./components/guides') 17 | })} 18 | {generateRoute({ 19 | paths: ['/protected'], 20 | component: require('./components/protected') 21 | })} 22 | {generateRoute({ 23 | paths: ['/profile/:id'], 24 | component: require('./userManager/components/profile') 25 | })} 26 | {generateRoute({ 27 | paths: ['/login-info'], 28 | component: require('./pages/login-info') 29 | })} 30 | {generateRoute({ 31 | paths: ['/beanList'], 32 | component: require('./beanManager/components/list') 33 | })} 34 | {generateRoute({ 35 | paths: ['postList'], 36 | component: require('./postManager/components/list') 37 | })} 38 | {generateRoute({ 39 | paths: ['postCreate'], 40 | component: require('./postManager/components/create') 41 | })} 42 | {generateRoute({ 43 | paths: ['/postOne/:id'], 44 | component: require('./postManager/components/single') 45 | })} 46 | {generateRoute({ 47 | paths: ['/postEdit/:id'], 48 | component: require('./postManager/components/edit') 49 | })} 50 | {generateRoute({ 51 | paths: ['/userCreate'], 52 | component: require('./userManager/components/create') 53 | })} 54 | {generateRoute({ 55 | paths: ['/commentList'], 56 | component: require('./commentManager/components/list') 57 | })} 58 | 59 | 60 | ); 61 | -------------------------------------------------------------------------------- /app/stores/auth.js: -------------------------------------------------------------------------------- 1 | class AuthStore { 2 | constructor() { 3 | this.bindActions(this.alt.getActions('auth')); 4 | this.authStatus = []; 5 | this.loginMessage = ''; 6 | } 7 | 8 | onLocalLoginFail(loginMessage) { 9 | return this.setState(loginMessage); 10 | } 11 | 12 | onLocalLoginSuccess(authStatus) { 13 | return this.setState({authStatus: authStatus.authStatus, loginMessage: authStatus.loginMessage}); 14 | } 15 | 16 | onFetchStatusSuccess(authStatus) { 17 | return this.setState({authStatus: authStatus, loginMessage: authStatus.isAuthenticated === true ? 'success' : ''}); 18 | } 19 | } 20 | 21 | export default AuthStore; 22 | -------------------------------------------------------------------------------- /app/stores/locale.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | class LocaleStore { 4 | constructor() { 5 | this.bindActions(this.alt.getActions('locale')); 6 | this.locales = ['']; 7 | this.messages = {}; 8 | } 9 | 10 | static getLocale() { 11 | return this.getState().locales[0]; 12 | } 13 | 14 | onSwitchLocaleSuccess(data: Object) { 15 | // Save locale into a cookie 16 | // that will be read from server on requests 17 | if (process.env.BROWSER) { 18 | const Cookies = require('cookies-js'); 19 | Cookies.set('_lang', data.locale, {expires: Infinity}); 20 | debug('dev')(`updated _lang cookie to ${data.locale}`); 21 | } 22 | 23 | return this.setState({ 24 | messages: data.messages, 25 | locales: [data.locale] 26 | }); 27 | } 28 | } 29 | 30 | export default LocaleStore; 31 | -------------------------------------------------------------------------------- /app/stores/page-title.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | class PageTitleStore { 4 | constructor() { 5 | this.bindActions(this.alt.getActions('page-title')); 6 | 7 | this.baseTitle = 'ISO-ReactJS'; 8 | this.delimeter = '|'; 9 | 10 | // Defaut title 11 | this.title = `${this.baseTitle}`; 12 | } 13 | 14 | onSet(title: ?string) { 15 | debug('dev')(`update page title to '${title}'`); 16 | return this.setState({title: `${this.baseTitle} ${this.delimeter} ${title}`}); 17 | } 18 | } 19 | 20 | export default PageTitleStore; 21 | -------------------------------------------------------------------------------- /app/stores/requests.js: -------------------------------------------------------------------------------- 1 | class RequestsStore { 2 | constructor() { 3 | this.bindActions(this.alt.getActions('requests')); 4 | this.inProgress = false; 5 | } 6 | 7 | onStart() { 8 | this._setInProgress(true); 9 | } 10 | 11 | onSuccess() { 12 | this._setInProgress(false); 13 | } 14 | 15 | onFail() { 16 | this._setInProgress(false); 17 | } 18 | 19 | _setInProgress(inProgress: boolean) { 20 | return this.setState({inProgress}); 21 | } 22 | } 23 | 24 | export default RequestsStore; 25 | -------------------------------------------------------------------------------- /app/styles/_application.scss: -------------------------------------------------------------------------------- 1 | $icon-font-path: "../../node_modules/bootstrap-sass/assets/fonts/bootstrap/"; 2 | 3 | @import "../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap"; 4 | -------------------------------------------------------------------------------- /app/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | $font-0: 'Roboto'; 2 | 3 | $url-0: url('../fonts/Roboto-Medium-webfont.eot'); 4 | $url-1: url('../fonts/Roboto-Medium-webfont.eot?#iefix'); 5 | $url-2: url('../fonts/Roboto-Medium-webfont.woff'); 6 | $url-3: url('../fonts/Roboto-Medium-webfont.ttf'); 7 | $url-4: url('../fonts/Roboto-Medium-webfont.svg#robotomedium'); 8 | 9 | @font-face { 10 | font-family: $font-0; 11 | src: $url-0; 12 | src: $url-1 format('embedded-opentype'), $url-2 format('woff'), $url-3 format('truetype'), $url-4 format('svg'); 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/styles/footer.scss: -------------------------------------------------------------------------------- 1 | .app--footer { 2 | margin-top: 50px; 3 | 4 | &-content { 5 | padding-top: 10px; 6 | text-align: center; 7 | } 8 | 9 | iframe { 10 | margin-right: 25px; 11 | 12 | &:last-child { 13 | margin-right: 0; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/styles/header.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins/hide-text'; 2 | @import 'mixins/reset-list'; 3 | 4 | .app-header { 5 | position: relative; 6 | margin-bottom: 40px; 7 | .app-logo { 8 | padding:0px 0px; 9 | position: relative; 10 | top:-3px; 11 | margin-right: 20px; 12 | 13 | img{ 14 | display: inline; 15 | } 16 | 17 | span{ 18 | position: relative; 19 | top: -8px; 20 | margin-left: 5px; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/styles/lang-picker.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins/reset-list'; 2 | 3 | $black: #000; 4 | $white: #fff; 5 | 6 | .lang--picker { 7 | @extend %reset-list; 8 | top: 15px; 9 | position: absolute; 10 | right: 15px; 11 | 12 | li { 13 | display: inline-block; 14 | margin-right: 10px; 15 | 16 | &:last-child { 17 | margin-right: 0; 18 | } 19 | 20 | a { 21 | cursor: pointer; 22 | opacity: .5; 23 | padding: 0 5px; 24 | text-transform: uppercase; 25 | } 26 | 27 | .active { 28 | background-color: $black; 29 | color: $white; 30 | opacity: 1; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import 'application'; 2 | // vendors 3 | @import '../../node_modules/modularized-normalize-scss/normalize'; 4 | 5 | // mixins / helpers 6 | @import 'fonts'; 7 | @import 'mixins/class-helper'; 8 | 9 | body { 10 | font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; 11 | } 12 | -------------------------------------------------------------------------------- /app/styles/mixins/_class-helper.scss: -------------------------------------------------------------------------------- 1 | .text-center { 2 | text-align: center; 3 | } 4 | 5 | .un-select { 6 | user-select: none; 7 | } 8 | -------------------------------------------------------------------------------- /app/styles/mixins/_hide-text.scss: -------------------------------------------------------------------------------- 1 | %hide-text { 2 | overflow: hidden; 3 | text-indent: 100%; 4 | white-space: nowrap; 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/mixins/_reset-list.scss: -------------------------------------------------------------------------------- 1 | %reset-list { 2 | list-style-type: none; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | -------------------------------------------------------------------------------- /app/styles/spinner.scss: -------------------------------------------------------------------------------- 1 | .app--spinner { 2 | background-image: url('../images/spinner.svg'); 3 | background-repeat: no-repeat; 4 | display: none; 5 | height: 44px; 6 | position: absolute; 7 | right: 10px; 8 | top: 10px; 9 | width: 44px; 10 | 11 | &.active { 12 | display: block; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/styles/test.scss: -------------------------------------------------------------------------------- 1 | body{ 2 | background-color: #DDD; 3 | } -------------------------------------------------------------------------------- /app/userManager/actions/resetpwd.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class ResetPwdActions { 5 | constructor() { 6 | this.generateActions( 7 | 'resetPasswordByAdminSuccess' 8 | ); 9 | } 10 | resetPasswordByAdmin(id:string, params) { 11 | const promise = (resolve) => { 12 | let that = this; 13 | that.alt.getActions('requests').start(); 14 | request.put(`${baseUrl}rest/user/resetPasswordByAdmin/${id}`) 15 | .send(params) 16 | .end((error, res) => { 17 | if (error) return resolve(error); 18 | this.actions.resetPasswordByAdminSuccess(res.body.success); 19 | this.alt.getActions('requests').success(); 20 | return resolve(); 21 | }, 300); 22 | }; 23 | this.alt.resolve(promise); 24 | } 25 | 26 | } 27 | 28 | export default ResetPwdActions; 29 | -------------------------------------------------------------------------------- /app/userManager/actions/role.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class RoleActions { 5 | constructor() { 6 | this.generateActions( 7 | 'fetchByAttributesSuccess', 'createSuccess' 8 | ); 9 | } 10 | fetchByAttributes(attribute: string) { 11 | const promise = (resolve) => { 12 | let that = this; 13 | that.alt.getActions('requests').start(); 14 | request.get(`${baseUrl}rest/role/${attribute}`) 15 | // .set('Accept', 'application/json') 16 | .end((error, res) => { 17 | if (error) return resolve(error); 18 | that.actions.fetchByAttributesSuccess(res.body.attributes); 19 | that.alt.getActions('requests').success(); 20 | return resolve(); 21 | }); 22 | }; 23 | this.alt.resolve(promise); 24 | } 25 | } 26 | 27 | export default RoleActions; 28 | -------------------------------------------------------------------------------- /app/userManager/actions/users.js: -------------------------------------------------------------------------------- 1 | import {baseUrl} from '../../../server/config/init'; 2 | import request from 'superagent'; 3 | 4 | class UsersActions { 5 | constructor() { 6 | this.generateActions( 7 | 'removeSuccess', 'fetchSuccess', 'createSuccess', 8 | 'fetchBySeedSuccess', 'updateActivatedSuccess' 9 | ); 10 | } 11 | create(params) { 12 | const promise = (resolve) => { 13 | // fake xhr 14 | this.alt.getActions('requests').start(); 15 | let role = params.role; 16 | delete params.role; 17 | request.post(`${baseUrl}rest/user`) 18 | .send(params) 19 | .end((error, res) => { 20 | if (error) return resolve(error); 21 | 22 | let createdUser = res.body.user; 23 | console.log(createdUser); 24 | request.post(`${baseUrl}rest/role`) 25 | .send({ 26 | authority: role, 27 | UserId: createdUser.id 28 | }) 29 | .end((error2) => { 30 | if (error2) return resolve(error2); 31 | this.actions.createSuccess(createdUser); 32 | this.alt.getActions('requests').success(); 33 | return resolve(); 34 | }); 35 | }, 300); 36 | }; 37 | this.alt.resolve(promise); 38 | } 39 | 40 | 41 | remove(id: number) { 42 | const promise = (resolve) => { 43 | let that = this; 44 | that.alt.getActions('requests').start(); 45 | 46 | request.del(`${baseUrl}rest/user/${id}`) 47 | .end(() => { 48 | that.actions.removeSuccess(id); 49 | that.alt.getActions('requests').success(); 50 | return resolve(); 51 | }); 52 | }; 53 | this.alt.resolve(promise); 54 | } 55 | 56 | 57 | fetch() { 58 | const promise = (resolve) => { 59 | let that = this; 60 | that.alt.getActions('requests').start(); 61 | 62 | request.get(`${baseUrl}rest/user`) 63 | // .set('Accept', 'application/json') 64 | .end((error, res) => { 65 | if (error) return resolve(error); 66 | that.actions.fetchSuccess(res.body.users); 67 | that.alt.getActions('requests').success(); 68 | return resolve(); 69 | }); 70 | }; 71 | this.alt.resolve(promise); 72 | } 73 | fetchBySeed(id: string) { 74 | const promise = (resolve) => { 75 | this.alt.getActions('requests').start(); 76 | request.get(`${baseUrl}rest/user/${id}`) 77 | .end((error, res) => { 78 | if (error) return resolve(error); 79 | const user: Object = res.body.user; 80 | this.actions.fetchBySeedSuccess(user); 81 | this.alt.getActions('requests').success(); 82 | return resolve(); 83 | }); 84 | }; 85 | 86 | this.alt.resolve(promise); 87 | } 88 | 89 | updateActivated(id:string, params) { 90 | const promise = (resolve) => { 91 | let that = this; 92 | that.alt.getActions('requests').start(); 93 | request.put(`${baseUrl}rest/user/activated/${id}`) 94 | .send(params) 95 | .end((error, res) => { 96 | if (error) return resolve(error); 97 | this.actions.updateActivatedSuccess(res.body.user); 98 | this.alt.getActions('requests').success(); 99 | return resolve(); 100 | }, 300); 101 | }; 102 | this.alt.resolve(promise); 103 | } 104 | 105 | } 106 | 107 | export default UsersActions; 108 | -------------------------------------------------------------------------------- /app/userManager/components/create.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React, {Component, PropTypes} from 'react'; 3 | import {IntlMixin} from 'react-intl'; 4 | import {Button, Input, Panel, Col} from 'react-bootstrap'; 5 | 6 | if (process.env.BROWSER) { 7 | require('userManager/styles/users.scss'); 8 | } 9 | 10 | class UserCreate extends Component { 11 | 12 | static contextTypes = { 13 | router: PropTypes.func 14 | } 15 | 16 | static propTypes = { 17 | flux: React.PropTypes.object.isRequired 18 | } 19 | 20 | _getIntlMessage = IntlMixin.getIntlMessage 21 | 22 | state = { 23 | error: false, 24 | role: null, 25 | posts: this.props.flux 26 | .getStore('users') 27 | .getState().user, 28 | }; 29 | 30 | handleSubmit(event) { 31 | event.preventDefault(); 32 | 33 | let newUser = { 34 | email: this.refs.email.refs.input.getDOMNode().value, 35 | password: this.refs.password.refs.input.getDOMNode().value, 36 | username: this.refs.username.refs.input.getDOMNode().value, 37 | role: this.state.role, 38 | }; 39 | this.props.flux.getActions('users').create(newUser); 40 | this.context.router.transitionTo('/'); 41 | } 42 | 43 | render() { 44 | return ( 45 | 46 | {this._getIntlMessage('userManager.add')}} 48 | footer={}> 51 |
    52 | 53 | 54 | 55 | Role 56 | 61 | {this.state.error && ( 62 |

    Bad login information

    63 | )} 64 | 65 |
    66 | 67 | ); 68 | } 69 | } 70 | 71 | const Role = React.createClass({ 72 | render () { 73 | return ( 74 |
    75 | 78 | 81 |
    82 | ) 83 | } 84 | }); 85 | 86 | 87 | export default UserCreate; 88 | -------------------------------------------------------------------------------- /app/userManager/components/profile.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {IntlMixin} from 'react-intl'; 3 | import capitalize from 'lodash/string/capitalize'; 4 | 5 | if (process.env.BROWSER) { 6 | require('userManager/styles/profile.scss'); 7 | } 8 | 9 | class Profile extends Component { 10 | 11 | static propTypes = { 12 | flux: PropTypes.object.isRequired, 13 | params: PropTypes.object.isRequired 14 | } 15 | 16 | _getIntlMessage = IntlMixin.getIntlMessage 17 | _formatMessage = IntlMixin.formatMessage.bind(Object.assign({}, this, IntlMixin)) 18 | 19 | state = this.props.flux 20 | .getStore('users') 21 | .getBySeed(this.props.params.id) 22 | 23 | componentWillMount() { 24 | this._setPageTitle(); 25 | 26 | this.props.flux 27 | .getActions('users') 28 | .fetchBySeed(this.props.params.id); 29 | } 30 | 31 | componentDidMount() { 32 | this.props.flux 33 | .getStore('users') 34 | .listen(this._handleStoreChange); 35 | } 36 | 37 | componentWillUnmount() { 38 | this.props.flux 39 | .getStore('users') 40 | .unlisten(this._handleStoreChange); 41 | } 42 | 43 | _handleStoreChange = () => { 44 | const user = this.props.flux 45 | .getStore('users') 46 | .getBySeed(this.props.params.id); 47 | 48 | this.setState(user); 49 | this._setPageTitle(); 50 | } 51 | 52 | _setPageTitle = () => { 53 | let title; 54 | if (this.state.user) { 55 | const user = this.state.user; 56 | const fullName = user.username; 57 | 58 | title = this._getIntlMessage('profile.page-title'); 59 | title = this._formatMessage(title, {fullName}); 60 | } 61 | else { 62 | title = this._getIntlMessage('profile.not-found-page-title'); 63 | } 64 | 65 | // Set page title 66 | this.props.flux 67 | .getActions('page-title') 68 | .set.defer(title); 69 | } 70 | 71 | _getFullName({first, last}) { 72 | return `${capitalize(first)} ${capitalize(last)}`; 73 | } 74 | 75 | render() { 76 | if (this.state.user) { 77 | const user: Object = this.state.user; 78 | const picture = JSON.parse(user.picture); 79 | let imgSrc = 'about:blank'; 80 | 81 | if (picture !== null) imgSrc = picture.medium; 82 | 83 | return ( 84 |
    85 |

    {user.username}

    86 | profile picture 89 |
    90 | ); 91 | } 92 | 93 | return ( 94 |

    User not found

    95 | ); 96 | } 97 | 98 | } 99 | 100 | export default Profile; 101 | -------------------------------------------------------------------------------- /app/userManager/components/resetpwd.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {Button, Modal, Glyphicon, Input, Alert} from 'react-bootstrap'; 3 | import TimerMixin from 'react-timer-mixin'; 4 | 5 | class ResetPwd extends Component { 6 | static propTypes = { 7 | flux: PropTypes.object.isRequired, 8 | userId: PropTypes.object.isRequired, 9 | parent: PropTypes.object.isRequired 10 | } 11 | 12 | parent = this.props.parent 13 | 14 | mixins: [TimerMixin] 15 | 16 | 17 | self= this; 18 | 19 | state = { 20 | showModal: false, 21 | isLoading: false, 22 | checkValue: false, 23 | success: false 24 | } 25 | 26 | componentDidMount() { 27 | this.props.flux 28 | .getStore('resetpwd') 29 | .listen(this._handleStoreChange); 30 | } 31 | 32 | componentWillUnmount() { 33 | this.props.flux 34 | .getStore('resetpwd') 35 | .unlisten(this._handleStoreChange); 36 | } 37 | 38 | _handleStoreChange = (state) => { 39 | if (state.success) { 40 | self.setTimeout(() => {this.setState( { showModal: false, success: false}); }, 1500); 41 | } 42 | return this.setState(state); 43 | } 44 | 45 | 46 | close = () => { 47 | this.setState({ showModal: false }); 48 | } 49 | 50 | open = () => { 51 | this.setState({ showModal: true }); 52 | } 53 | 54 | handleSubmit = () => { 55 | this.setState({isLoading: true}); 56 | 57 | let newPwd = { 58 | pwd1: this.refs.pwd1.refs.input.getDOMNode().value, 59 | pwd2: this.refs.pwd2.refs.input.getDOMNode().value 60 | }; 61 | 62 | if (this.state.checkValue) { 63 | this.props.flux.getActions('resetpwd').resetPasswordByAdmin( this.props.userId, newPwd); 64 | 65 | // Reset checkValue 66 | this.setState({checkValue: false}); 67 | 68 | // Clear Input 69 | this.refs.pwd1.refs.input.getDOMNode().value = ''; 70 | this.refs.pwd2.refs.input.getDOMNode().value = ''; 71 | } 72 | } 73 | 74 | handleCheckValue() { 75 | let newPwd = { 76 | pwd1: this.refs.pwd1.refs.input.getDOMNode().value, 77 | pwd2: this.refs.pwd2.refs.input.getDOMNode().value 78 | }; 79 | 80 | this.setState({checkValue: false}); 81 | // Not Empty 82 | if (newPwd.pwd1 !== '' || newPwd.pwd2 !== '') { 83 | // Check Password 84 | if (newPwd.pwd1 === newPwd.pwd2) { 85 | this.setState({checkValue: true}); 86 | } 87 | } 88 | } 89 | 90 | render() { 91 | let isLoading = this.state.isLoading; 92 | let btnStyle = this.state.checkValue ? 'primary' : ''; 93 | let btnText = isLoading ? this.parent._getIntlMessage('userManager.resetPassword.resetting') : this.parent._getIntlMessage('userManager.resetPassword.reset'); 94 | 95 | 96 | let inputStyle = this.state.checkValue ? 'success' : ''; 97 | return ( 98 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | ); 138 | } 139 | } 140 | 141 | export default ResetPwd; 142 | -------------------------------------------------------------------------------- /app/userManager/components/users.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react'; 2 | import {Link} from 'react-router'; 3 | import {IntlMixin} from 'react-intl'; 4 | import ResetPwd from 'userManager/components/resetpwd'; 5 | import {Button, Table, Panel, Col, Glyphicon, ButtonToolbar} from 'react-bootstrap'; 6 | 7 | if (process.env.BROWSER) { 8 | require('userManager/styles/users.scss'); 9 | } 10 | 11 | class Users extends Component { 12 | 13 | static propTypes = { 14 | flux: PropTypes.object.isRequired 15 | } 16 | 17 | _getIntlMessage = IntlMixin.getIntlMessage 18 | 19 | state = { 20 | users: this.props.flux 21 | .getStore('users') 22 | .getState().users, 23 | authStatus: this.props.flux 24 | .getStore('auth') 25 | .getState().authStatus 26 | }; 27 | 28 | componentWillMount() { 29 | this.props.flux 30 | .getActions('page-title') 31 | .set(this._getIntlMessage('userManager.page-title')); 32 | 33 | this.props.flux 34 | .getActions('users') 35 | .fetch(); 36 | } 37 | 38 | componentDidMount() { 39 | this.props.flux 40 | .getStore('users') 41 | .listen(this._handleStoreChange); 42 | } 43 | 44 | componentWillUnmount() { 45 | this.props.flux 46 | .getStore('users') 47 | .unlisten(this._handleStoreChange); 48 | } 49 | 50 | _handleStoreChange = (state) => { 51 | state.authStatus = this.props.flux.getStore('auth').getState().authStatus; 52 | return this.setState(state); 53 | } 54 | 55 | _removeUser(id) { 56 | this.props.flux 57 | .getActions('users') 58 | .remove(id); 59 | } 60 | 61 | _updateActivated(id, activated) { 62 | activated = ( activated === true ? false : true ); 63 | this.props.flux 64 | .getActions('users') 65 | .updateActivated(id, {activated: activated}); 66 | } 67 | 68 | renderActivated(user) { 69 | let lockStyle = user.activated === true ? 'success' : 'danger'; 70 | 71 | if (this.state.authStatus.authority === 'admin') { 72 | let myId = this.state.authStatus.sessionUser.id; 73 | if (myId !== user.id) { 74 | return ( 75 | 78 | ); 79 | } 80 | } 81 | } 82 | 83 | renderUser = (user, index) => { 84 | return ( 85 | 86 | {user.email} 87 | 88 | 89 | 90 | 91 | 92 | 93 | {() => { 94 | if (this.state.authStatus.authority === 'admin') { 95 | return ( 96 | 100 | ); 101 | } 102 | }()} 103 | {() => { 104 | if (this.state.authStatus.authority === 'admin') { 105 | return ( 106 | 107 | ); 108 | } 109 | }()} 110 | 111 | {this.renderActivated(user)} 112 | 113 | 114 | 115 | 116 | ); 117 | } 118 | 119 | render() { 120 | return ( 121 | 122 | {this._getIntlMessage('userManager.title')}} 124 | footer={() => { 125 | if (this.state.authStatus.authority === 'admin') { 126 | return ( 127 | 128 | 131 | 132 | ); 133 | } 134 | }()} 135 | > 136 | 137 | 138 | 139 | 142 | 145 | 146 | 147 | 148 | {this.state.users.map(this.renderUser)} 149 | 150 |
    140 | {this._getIntlMessage('userManager.email')} 141 | 143 | {this._getIntlMessage('userManager.actions')} 144 |
    151 |
    152 | 153 | ); 154 | } 155 | } 156 | 157 | export default Users; 158 | -------------------------------------------------------------------------------- /app/userManager/stores/resetpwd.js: -------------------------------------------------------------------------------- 1 | class ResetPwdStore { 2 | 3 | 4 | constructor() { 5 | this.bindActions(this.alt.getActions('resetpwd')); 6 | } 7 | onResetPasswordByAdminSuccess(success) { 8 | return this.setState({ 9 | isLoading: false, 10 | checkValue: false, 11 | success: success 12 | }); 13 | } 14 | 15 | } 16 | 17 | export default ResetPwdStore; 18 | -------------------------------------------------------------------------------- /app/userManager/stores/role.js: -------------------------------------------------------------------------------- 1 | class RoleStore { 2 | 3 | constructor() { 4 | this.bindActions(this.alt.getActions('role')); 5 | this.attributes = []; 6 | } 7 | 8 | onFetchByAttributesSuccess(attributes) { 9 | return this.setState({attributes}); 10 | } 11 | 12 | } 13 | 14 | export default RoleStore; 15 | -------------------------------------------------------------------------------- /app/userManager/stores/users.js: -------------------------------------------------------------------------------- 1 | import isEmpty from 'lodash/array/findIndex'; 2 | 3 | class UsersStore { 4 | 5 | constructor() { 6 | this.bindActions(this.alt.getActions('users')); 7 | this.users = []; 8 | } 9 | 10 | static getBySeed(id) { 11 | const users: Array = this.getState().users; 12 | let singleUser = this.getState().user; 13 | if (singleUser === null) singleUser = users.find((user) => user.id.toString() === id.toString()); 14 | return {user: singleUser}; 15 | } 16 | 17 | onRemoveSuccess(id) { 18 | const users: Array = this.users.slice(); 19 | let removeIndex = -1; 20 | users.forEach((user, index) => { 21 | if (user.id === id) removeIndex = index; 22 | }); 23 | 24 | if (removeIndex >= 0) users.splice(removeIndex, 1); 25 | 26 | return this.setState({users}); 27 | } 28 | 29 | onCreateSuccess(user) { 30 | const users: Array = this.users.slice(); 31 | users.push(user); 32 | 33 | return this.setState({users}); 34 | } 35 | 36 | onFetchSuccess(users) { 37 | if (isEmpty(this.users)) { 38 | // just apply the new users 39 | // this is called on every server rendering 40 | return this.setState({users}); 41 | } 42 | 43 | const merged: Array = this.users.slice(); 44 | users.forEach((user) => { 45 | let match: ?Object = merged.find((u) => u.seed === user.seed) || null; 46 | if (match) { 47 | match = user; 48 | } 49 | // push the new user 50 | else { 51 | merged.push(user); 52 | } 53 | }); 54 | 55 | return this.setState({users: merged}); 56 | } 57 | 58 | onFetchBySeedSuccess(user) { 59 | return this.setState({user}); 60 | } 61 | 62 | onUpdateActivatedSuccess(editUser) { 63 | const users: Array = this.users; 64 | users.forEach((user, index) => { 65 | if (user.id === editUser.id) { 66 | this.users[index] = editUser; 67 | } 68 | }); 69 | return this.setState({users}); 70 | } 71 | 72 | } 73 | 74 | export default UsersStore; 75 | -------------------------------------------------------------------------------- /app/userManager/styles/profile.scss: -------------------------------------------------------------------------------- 1 | .app--profile { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /app/userManager/styles/users.scss: -------------------------------------------------------------------------------- 1 | .app-users { 2 | .panel-footer { 3 | text-align: center; 4 | } 5 | 6 | .table { 7 | margin-bottom: 0px !important; 8 | 9 | tr { 10 | td, th { 11 | padding: 5px 10px; 12 | vertical-align: middle !important; 13 | text-align: center; 14 | &:first-child{ 15 | width: 60%; 16 | } 17 | } 18 | } 19 | } 20 | 21 | .user-remove{ 22 | margin-left: 15px; 23 | } 24 | } 25 | 26 | .role{ 27 | font-weight: bold; 28 | } 29 | -------------------------------------------------------------------------------- /app/utils/alt-resolver.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import noop from 'lodash/utility/noop'; 3 | 4 | import React from 'react'; 5 | import Iso from 'iso'; 6 | 7 | import ErrorPage from 'pages/server-error'; 8 | 9 | class AltResolver { 10 | 11 | _toResolve = [] 12 | _firstClientSideRender = !(process.env.NODE_ENV === 'test') 13 | 14 | resolve(promise: Function, later = false) { 15 | if (process.env.BROWSER && !later) { 16 | // Prevent first app mount to re-resolve same 17 | // promises that server already did 18 | if (this._firstClientSideRender) { 19 | return noop(); 20 | } 21 | 22 | return new Promise(promise); 23 | } 24 | 25 | return this._toResolve.push(promise); 26 | } 27 | 28 | mapPromises() { 29 | return this._toResolve.map((promise) => new Promise(promise)); 30 | } 31 | 32 | async render(Handler, flux, force = false) { 33 | if (process.env.BROWSER && !force) { 34 | debug('dev')('`altResolver.render` should not be used in browser, something went wrong'); 35 | return null; 36 | } 37 | 38 | let content; 39 | try { 40 | // Fire first render to collect XHR promises 41 | let {authStatus} = flux.getStore('auth').getState(); 42 | debug('dev')('first render'); 43 | React.renderToString(Handler); 44 | 45 | // Get the promises collected from the first rendering 46 | const promises = this.mapPromises(); 47 | 48 | // Resolve all promises collected 49 | await Promise.all(promises); 50 | 51 | debug('dev')('second render'); 52 | // Get the new content with promises resolved 53 | flux.getActions('auth').fetchStatusSuccess(authStatus); 54 | const fluxSnapshot = flux.takeSnapshot(); 55 | const app = React.renderToString(Handler); 56 | const {title} = flux.getStore('page-title').getState(); 57 | 58 | // Render the html with state in it 59 | content = {body: Iso.render(app, fluxSnapshot), title}; 60 | } 61 | catch (error) { 62 | // catch script error, render 500 page 63 | debug('koa')('`rendering error`'); 64 | debug('koa')(error); 65 | 66 | const fluxSnapshot = flux.takeSnapshot(); 67 | const app = React.renderToString(React.createElement(ErrorPage)); 68 | const {title} = flux.getStore('page-title').getState(); 69 | 70 | content = {body: Iso.render(app, fluxSnapshot), title}; 71 | } 72 | 73 | // return the content 74 | return content; 75 | } 76 | 77 | } 78 | 79 | export default AltResolver; 80 | -------------------------------------------------------------------------------- /app/utils/flux.js: -------------------------------------------------------------------------------- 1 | import Alt from 'alt'; 2 | import AltResolver from './alt-resolver.js'; 3 | 4 | class Flux extends Alt { 5 | 6 | constructor(config = {}) { 7 | super(config); 8 | 9 | this._resolver = new AltResolver(); 10 | 11 | // Register Actions 12 | this.addActions('users', require('userManager/actions/users')); 13 | this.addActions('posts', require('postManager/actions/posts')); 14 | this.addActions('bean', require('beanManager/actions/bean')); 15 | this.addActions('comment', require('commentManager/actions/comment')); 16 | this.addActions('role', require('userManager/actions/role')); 17 | this.addActions('resetpwd', require('userManager/actions/resetpwd')); 18 | 19 | // Register Stores 20 | this.addStore('users', require('userManager/stores/users')); 21 | this.addStore('posts', require('postManager/stores/posts')); 22 | this.addStore('bean', require('beanManager/stores/bean')); 23 | this.addStore('comment', require('commentManager/stores/comment')); 24 | this.addStore('role', require('userManager/stores/role')); 25 | this.addStore('resetpwd', require('userManager/stores/resetpwd')); 26 | 27 | ['requests', 'locale', 'page-title', 'auth'] 28 | .map(this.registerCouple); 29 | } 30 | 31 | 32 | registerCouple = ::this.registerCouple 33 | registerCouple(name) { 34 | this.addActions(name, require(`actions/${name}`)); 35 | this.addStore(name, require(`stores/${name}`)); 36 | } 37 | 38 | resolve(result) { 39 | this._resolver.resolve(result); 40 | } 41 | 42 | render(handler) { 43 | return this._resolver.render(handler, this); 44 | } 45 | } 46 | 47 | export default Flux; 48 | -------------------------------------------------------------------------------- /app/utils/image-resolver.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | export default (imagePath) => { 4 | if (process.env.BROWSER) { 5 | debug('dev')('`image-resolver` should not be used in browser, something went wrong'); 6 | throw new Error('image-resolver called on browser'); 7 | } 8 | else { 9 | // Load images compiled from `webpack-stats` 10 | // don't cache the `webpack-stats.json` on dev 11 | // so we gonna read the file on each request 12 | let images; 13 | if (process.env.NODE_ENV === 'development') { 14 | const fs = require('fs'); 15 | const path = require('path'); 16 | images = fs.readFileSync(path.resolve(__dirname, '../../server/webpack-stats.json')); 17 | images = JSON.parse(images).images; 18 | } 19 | // on production, use simple `require` to cache the file 20 | else { 21 | images = require('../../server/webpack-stats.json').images; 22 | } 23 | 24 | // Find the correct image 25 | const regex = new RegExp(`${imagePath}$`); 26 | const image = images.find(img => regex.test(img.original)); 27 | 28 | // Serve image. 29 | if (image) return image.compiled; 30 | 31 | // Serve a not-found asset maybe? 32 | return ''; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /app/utils/intl-loader.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | // We need to define `ReactIntl` on the global scope 4 | // in order to load specific locale data from `ReactIntl` 5 | // see: https://github.com/iam4x/isomorphic-flux-boilerplate/issues/64 6 | if (process.env.BROWSER) window.ReactIntl = require('react-intl'); 7 | 8 | const loaders = { 9 | 10 | en(callback, force = false) { 11 | if (!window.Intl || force) { 12 | require.ensure([ 13 | 'intl', 14 | 'intl/locale-data/jsonp/en.js', 15 | 'data/en' 16 | ], (require) => { 17 | require('intl'); 18 | require('intl/locale-data/jsonp/en.js'); 19 | const lang = require('data/en'); 20 | return callback(lang); 21 | }); 22 | } 23 | else { 24 | require.ensure([ 25 | 'react-intl/dist/locale-data/en.js', 26 | 'data/en' 27 | ], (require) => { 28 | require('react-intl/dist/locale-data/en.js'); 29 | const lang = require('data/en'); 30 | return callback(lang); 31 | }); 32 | } 33 | }, 34 | 35 | fr(callback, force = false) { 36 | if (!window.Intl || force) { 37 | require.ensure([ 38 | 'intl', 39 | 'intl/locale-data/jsonp/fr.js', 40 | 'data/fr' 41 | ], (require) => { 42 | require('intl'); 43 | require('intl/locale-data/jsonp/fr.js'); 44 | const lang = require('data/fr'); 45 | return callback(lang); 46 | }); 47 | } 48 | else { 49 | require.ensure([ 50 | 'react-intl/dist/locale-data/fr.js', 51 | 'data/fr' 52 | ], (require) => { 53 | require('react-intl/dist/locale-data/fr.js'); 54 | const lang = require('data/fr'); 55 | return callback(lang); 56 | }); 57 | } 58 | } 59 | 60 | }; 61 | 62 | export default (locale, force) => { 63 | debug('dev')(`loading lang ${locale}`); 64 | return new Promise((resolve) => loaders[locale](resolve, force)); 65 | }; 66 | -------------------------------------------------------------------------------- /app/utils/intl-polyfill.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | const hasBuiltInLocaleData = (locale) => { 4 | return Intl.NumberFormat.supportedLocalesOf(locale)[0] === locale && Intl.DateTimeFormat.supportedLocalesOf(locale)[0] === locale; 5 | }; 6 | 7 | export default (locales) => { 8 | if (!process.env.BROWSER) { 9 | if (global.Intl) { 10 | if (!locales.every(hasBuiltInLocaleData)) { 11 | // `Intl` exists, but it doesn't have the data we need, so load the 12 | // polyfill and replace the constructors with need with the polyfill's. 13 | const IntlPolyfill = require('intl'); 14 | Intl.NumberFormat = IntlPolyfill.NumberFormat; 15 | Intl.DateTimeFormat = IntlPolyfill.DateTimeFormat; 16 | } 17 | } 18 | else { 19 | // No `Intl`: use and load polyfill 20 | global.Intl = require('intl'); 21 | debug('koa')('Intl is not supported, so the polyfill has been loaded'); 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /app/utils/localized-routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Route} from 'react-router'; 3 | 4 | // Return an array of different routes paths for a same component 5 | // used to generate localized routes in `/app/routes.js` 6 | export function generateRoute({paths, component}) { 7 | return paths.map(function(path) { 8 | const props = {key: path, path, component}; 9 | // Static `onEnter` is defined on 10 | // component, we should pass it to route props 11 | if (component.onEnter) props.onEnter = component.onEnter; 12 | return ; 13 | }); 14 | } 15 | 16 | // Replace params in route format: `/profile/:seed` 17 | // 18 | // Params: 19 | // - route: string = route with `:` preceding params 20 | // - params: object = key are param names 21 | // 22 | // Example: 23 | // For route: `/route/:foo` params will be `{foo: 'bar'}` 24 | export function replaceParams(route, params) { 25 | let parsedRoute = route.trim(); 26 | Object.keys(params).forEach(function(paramKey) { 27 | const param = ':' + paramKey; 28 | const paramValue = params[paramKey]; 29 | if (parsedRoute && parsedRoute.match(param)) { 30 | parsedRoute = parsedRoute.replace(param, paramValue); 31 | } 32 | }); 33 | return parsedRoute; 34 | } 35 | -------------------------------------------------------------------------------- /app/utils/promisify.js: -------------------------------------------------------------------------------- 1 | export default (fn) => new Promise((resolve) => fn((result) => resolve(result))); 2 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | var webpack = require('webpack'); 4 | 5 | var coverage; 6 | var reporters; 7 | if (process.env.CONTINUOUS_INTEGRATION) { 8 | coverage = { 9 | type: 'lcov', 10 | dir: 'coverage/' 11 | }; 12 | reporters = ['coverage', 'coveralls']; 13 | } 14 | else { 15 | coverage = { 16 | type: 'html', 17 | dir: 'coverage/' 18 | }; 19 | reporters = ['progress', 'coverage']; 20 | } 21 | 22 | module.exports = function (config) { 23 | config.set({ 24 | browsers: ['Firefox'], 25 | browserNoActivityTimeout: 30000, 26 | frameworks: ['mocha', 'chai', 'sinon-chai'], 27 | files: ['tests.webpack.js'], 28 | preprocessors: {'tests.webpack.js': ['webpack', 'sourcemap']}, 29 | reporters: reporters, 30 | coverageReporter: coverage, 31 | webpack: { 32 | devtool: 'inline-source-map', 33 | module: { 34 | loaders: [ 35 | // TODO: fix sourcemaps 36 | // see: https://github.com/deepsweet/isparta-loader/issues/1 37 | { 38 | test: /\.js$|.jsx$/, 39 | loader: 'babel', 40 | exclude: /node_modules/ 41 | }, 42 | { 43 | test: /\.js$|.jsx$/, 44 | loader: 'isparta?{babel: {stage: 0}}', 45 | exclude: /node_modules|test/ 46 | }, 47 | { 48 | test: /\.scss|.css$/, 49 | loader: 'style!css!sass' 50 | }, 51 | { 52 | test: /\.(jpe?g|png|gif|svg|woff|woff2|eot|ttf)$/, 53 | loader: 'file?name=[sha512:hash:base64:7].[ext]' 54 | }, 55 | { 56 | test: /\.json$/, loader: 'json' 57 | } 58 | ] 59 | }, 60 | plugins: [ 61 | new webpack.DefinePlugin({ 62 | 'process.env': { 63 | BROWSER: JSON.stringify(true), 64 | NODE_ENV: JSON.stringify('test') 65 | } 66 | }) 67 | ], 68 | resolve: { 69 | extensions: ['', '.js', '.json', '.jsx'], 70 | modulesDirectories: ['node_modules', 'app'] 71 | } 72 | }, 73 | webpackServer: { 74 | noInfo: true 75 | } 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "isomorphic-flux-boilerplate", 3 | "version": "0.3.0", 4 | "description": "An ES7 isomorphic Flux/ReactJS boilerplate", 5 | "main": "server/index.js", 6 | "scripts": { 7 | "build": "babel-node ./node_modules/.bin/webpack --stats --config ./webpack/prod.config.js", 8 | "dev": "babel-node ./webpack/dev-server.js", 9 | "test": "babel-node ./node_modules/.bin/karma start --single-run", 10 | "dev-test": "babel-node ./node_modules/.bin/karma start", 11 | "lint": "eslint app server webpack shared --ext .jsx,.js", 12 | "rest-test": "NODE_ENV=test ./node_modules/.bin/mocha --opts ./test/server/mocha.opts --compilers js:babel/register ./test/server/**/*.spec.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/iam4x/isomorphic-flux-boilerplate.git" 17 | }, 18 | "homepage": "https://github.com/iam4x/isomorphic-flux-boilerplate", 19 | "bugs": "https://github.com/iam4x/isomorphic-flux-boilerplate/issues", 20 | "author": "iam4x", 21 | "license": "MIT", 22 | "dependencies": { 23 | "alloyeditor": "^0.3.7", 24 | "alt": "^0.17.1", 25 | "babel": "^5.8.3", 26 | "blocked": "^1.1.0", 27 | "bootstrap-sass": "^3.3.5", 28 | "classnames": "^2.1.3", 29 | "co-busboy": "^1.3.0", 30 | "cookies-js": "^1.2.1", 31 | "debug": "^2.2.0", 32 | "fbjs": "^0.4.0", 33 | "fs-extra": "^0.22.1", 34 | "history": "^1.13.1", 35 | "intl": "^1.0.0", 36 | "iso": "^4.1.0", 37 | "koa": "^1.0.0", 38 | "koa-bodyparser": "^2.0.0", 39 | "koa-cash": "^1.0.1", 40 | "koa-compressor": "^1.0.3", 41 | "koa-conditional-get": "^1.0.3", 42 | "koa-cors": "0.0.16", 43 | "koa-etag": "^2.0.0", 44 | "koa-favicon": "^1.2.0", 45 | "koa-generic-session": "^1.9.0", 46 | "koa-hbs": "^0.7.0", 47 | "koa-helmet": "^0.2.0", 48 | "koa-jade": "^1.1.0", 49 | "koa-logger": "^1.3.0", 50 | "koa-mount": "^1.3.0", 51 | "koa-passport": "^1.1.6", 52 | "koa-proxy": "^0.3.0", 53 | "koa-response-time": "^1.0.2", 54 | "koa-router": "^5.1.2", 55 | "koa-static": "^1.4.9", 56 | "koa-static-cache": "^3.1.2", 57 | "lodash": "^3.10.1", 58 | "material-ui": "^0.11.0", 59 | "passport-facebook": "^2.0.0", 60 | "passport-local": "^1.0.0", 61 | "react": "^0.14.0", 62 | "react-bootstrap": "^0.24.2", 63 | "react-dom": "^0.14.0", 64 | "react-dropzone": "^1.2.3", 65 | "react-intl": "^1.2.0", 66 | "react-router": "1.0.0-beta3", 67 | "react-tagsinput": "^1.4.0", 68 | "react-tap-event-plugin": "^0.1.7", 69 | "react-timer-mixin": "^0.13.2", 70 | "sequelize": "^3.4.1", 71 | "sqlite3": "^3.0.9", 72 | "superagent": "^1.2.0" 73 | }, 74 | "devDependencies": { 75 | "autoprefixer-loader": "^2.0.0", 76 | "babel-core": "^5.8.3", 77 | "babel-eslint": "^4.0.5", 78 | "babel-loader": "^5.3.2", 79 | "browser-sync": "^2.8.0", 80 | "css-loader": "^0.15.5", 81 | "chai": "^3.2.0", 82 | "del": "^1.2.0", 83 | "dev-ip": "^1.0.1", 84 | "eslint": "^1.4.1", 85 | "eslint-config-airbnb": "^1.0.0", 86 | "eslint-loader": "^1.0.0", 87 | "eslint-plugin-react": "^3.0.0", 88 | "esprima-fb": "^15001.1.0-dev-harmony-fb", 89 | "extract-text-webpack-plugin": "^0.8.2", 90 | "file-loader": "^0.8.4", 91 | "image-webpack-loader": "^1.6.1", 92 | "imagemin": "^3.2.0", 93 | "isparta-loader": "^0.2.0", 94 | "json-loader": "^0.5.2", 95 | "karma": "^0.13.3", 96 | "karma-chai-plugins": "^0.6.0", 97 | "karma-chrome-launcher": "^0.2.0", 98 | "karma-cli": "^0.1.0", 99 | "karma-coverage": "^0.4.2", 100 | "karma-coveralls": "^1.1.2", 101 | "karma-firefox-launcher": "^0.1.6", 102 | "karma-mocha": "^0.2.0", 103 | "karma-sourcemap-loader": "^0.3.5", 104 | "karma-webpack": "^1.6.0", 105 | "mocha": "^2.2.5", 106 | "node-sass": "^3.2.0", 107 | "node-watch": "^0.3.4", 108 | "react-hot-loader": "^1.2.8", 109 | "sass-loader": "^1.0.3", 110 | "style-loader": "^0.12.3", 111 | "supertest": "^1.0.1", 112 | "sinon": "^1.15.4", 113 | "webpack": "^1.10.5", 114 | "webpack-dev-server": "^1.10.1" 115 | }, 116 | "tags": [ 117 | "react", 118 | "flux", 119 | "alt", 120 | "iso", 121 | "isomorphic", 122 | "spa", 123 | "ssr", 124 | "koa", 125 | "webpack", 126 | "jsx", 127 | "es6", 128 | "babel", 129 | "sass", 130 | "scss", 131 | "eslint", 132 | "react-router", 133 | "react-hot-loader", 134 | "hot-loader", 135 | "async", 136 | "server", 137 | "generator", 138 | "es7", 139 | "i18n", 140 | "intl" 141 | ], 142 | "keywords": [ 143 | "react", 144 | "flux", 145 | "alt", 146 | "iso", 147 | "isomorphic", 148 | "spa", 149 | "ssr", 150 | "koa", 151 | "webpack", 152 | "jsx", 153 | "es6", 154 | "babel", 155 | "sass", 156 | "scss", 157 | "eslint", 158 | "react-router", 159 | "react-hot-loader", 160 | "hot-loader", 161 | "async", 162 | "server", 163 | "generator", 164 | "es7", 165 | "i18n", 166 | "intl" 167 | ], 168 | "engines": { 169 | "iojs": "^2.3.0", 170 | "nodejs": "^4.2.2" 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps": [ 3 | { 4 | "name": "app", 5 | "script": "./server/index.js", 6 | "instances": 1, 7 | "log_date_format": "YYYY-MM-DD HH:mm Z", 8 | "log_file": "./logs/app.log", 9 | "error_file": "./logs/app-err.log", 10 | "out_file": "./logs/app-out.log", 11 | "exec_mode": "cluster_mode", 12 | "max_memory_restart": "500M", 13 | "watch": false, 14 | "env": { 15 | "NODE_ENV": "production", 16 | "DEBUG": "koa" 17 | }, 18 | "node_args": "--max_old_space_size=300 --harmony" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | var passport = require('koa-passport') 2 | 3 | var user = { id: 1, username: 'test' } 4 | 5 | passport.serializeUser(function(user, done) { 6 | done(null, user) 7 | }) 8 | 9 | passport.deserializeUser(function(id, done) { 10 | done(null, user) 11 | }) 12 | 13 | 14 | var FacebookStrategy = require('passport-facebook').Strategy 15 | passport.use(new FacebookStrategy({ 16 | clientID: '1430508653909057', 17 | clientSecret: '55ddd1f9c5f3fa10851064797dacd8a0', 18 | callbackURL: 'http://localhost:' + (process.env.PORT || 3000) + '/auth/facebook/callback' 19 | }, 20 | function(token, tokenSecret, profile, done) { 21 | // retrieve user ... 22 | console.log("token", token); 23 | console.log("tokenSecret", tokenSecret); 24 | console.log("profile", profile); 25 | 26 | done(null, user) 27 | } 28 | )); 29 | 30 | var LocalStrategy = require('passport-local').Strategy 31 | passport.use(new LocalStrategy(async (username, password, done) => { 32 | let loginInfo = { 33 | where: {username, password}, 34 | include: [models.Role] 35 | }; 36 | try { 37 | let logedUser = (await models.User.findOne(loginInfo)).dataValues; 38 | 39 | console.log('logedUser', logedUser); 40 | 41 | if (logedUser) { 42 | done(null, logedUser) 43 | } else { 44 | done(null, false); 45 | } 46 | } catch (e) { 47 | console.log(e); 48 | done(null, false); 49 | } 50 | })); 51 | -------------------------------------------------------------------------------- /server/bootstrap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export default async (cb) => { 4 | 5 | let picture = { 6 | "large":"http://api.randomuser.me/portraits/women/72.jpg", 7 | "medium":"http://api.randomuser.me/portraits/med/women/72.jpg", 8 | "thumbnail":"http://api.randomuser.me/portraits/thumb/women/72.jpg" 9 | } 10 | 11 | let visitorUser = { 12 | "username":"visitor", 13 | "password":"visitor", 14 | "gender":"male", 15 | "email":"visitor@visitor.com", 16 | "phone":"(951)-385-6121", 17 | "cell":"(657)-919-3511", 18 | "picture":JSON.stringify(picture), 19 | 'activated':true 20 | } 21 | 22 | 23 | let editorUser = { 24 | "username":"editor", 25 | "password":"editor", 26 | "gender":"male", 27 | "email":"editor@editor.com", 28 | "phone":"(951)-385-6121", 29 | "cell":"(657)-919-3511", 30 | "picture":JSON.stringify(picture), 31 | 'activated':true 32 | } 33 | 34 | let editorUser2 = { 35 | "username":"editor2", 36 | "password":"editor2", 37 | "gender":"male", 38 | "email":"editor2@editor.com", 39 | "phone":"(951)-385-6121", 40 | "cell":"(657)-919-3511", 41 | "picture":JSON.stringify(picture), 42 | 'activated':true 43 | } 44 | 45 | let adminUser = { 46 | "username":"admin", 47 | "password":"admin", 48 | "gender":"male", 49 | "email":"admin@admin.com", 50 | "phone":"(951)-385-6121", 51 | "cell":"(657)-919-3511", 52 | "picture":JSON.stringify(picture), 53 | 'activated':true 54 | } 55 | 56 | let adminUser2 = { 57 | "username":"admin2", 58 | "password":"admin2", 59 | "gender":"male", 60 | "email":"admin2@admin.com", 61 | "phone":"(951)-385-6121", 62 | "cell":"(657)-919-3511", 63 | "picture":JSON.stringify(picture), 64 | 'activated':false 65 | } 66 | 67 | 68 | let newBeanBlack = { 69 | name: '黑珍珠', 70 | place: '哥斯大黎加 Costa Rica', 71 | breed: '卡杜拉 Caturra;卡杜艾 Catuai', 72 | manor: '查孔自家農場', 73 | method: '自然處理法;「黑珍珠」', 74 | roastDate: '2014年', 75 | roastType: '烘焙度', 76 | flavour: '風味', 77 | lat: 25.0339031, 78 | lon: 121.5645099, 79 | coverUrl: '', 80 | thumbCoverUrl: '' 81 | } 82 | 83 | let newBeanWhite = { 84 | name: '白珍珠', 85 | place: '哥斯大黎加 Costa Rica', 86 | breed: '卡杜拉 Caturra;卡杜艾 Catuai', 87 | manor: '查孔自家農場', 88 | method: '自然處理法;「黑珍珠」', 89 | roastDate: '2014年', 90 | roastType: '烘焙度', 91 | flavour: '風味', 92 | lat: 25.0339031, 93 | lon: 121.5645099, 94 | coverUrl: '', 95 | thumbCoverUrl: '' 96 | } 97 | 98 | let testPost1 = { 99 | title: '這是一篇文章', 100 | content: '西捨帥帥' 101 | } 102 | 103 | let testPost2 = { 104 | title: '這是兩篇文章', 105 | content: '蹤影帥帥' 106 | } 107 | 108 | let testPost3 = { 109 | title: '這是三篇文章', 110 | content: '三篇文章的內容' 111 | } 112 | 113 | let Comment1 = { 114 | author: '西捨', 115 | content: '我也這麼認為' 116 | } 117 | 118 | let Comment2 = { 119 | author: 'Rick', 120 | content: '感謝大大無私分享' 121 | } 122 | 123 | let Comment3 = { 124 | author: '是叉貓', 125 | content: '肥宅4ni' 126 | } 127 | 128 | let adminRole = { 129 | authority: 'admin' 130 | }; 131 | 132 | let editorRole = { 133 | authority: 'editor' 134 | }; 135 | 136 | let tag1 = { 137 | name: 'Ian shuai shuai de' 138 | }; 139 | 140 | let tag2 = { 141 | name: 'Tz shuai shuai de' 142 | }; 143 | 144 | let tag3 = { 145 | name: 'this is a tag' 146 | }; 147 | 148 | 149 | try { 150 | let createdVisitor = await models.User.create(visitorUser); 151 | let createdEditor = await models.User.create(editorUser); 152 | let createdEditor2 = await models.User.create(editorUser2); 153 | let createdAdmin = await models.User.create(adminUser); 154 | let createdAdmin2 = await models.User.create(adminUser2); 155 | 156 | let createdEditorRole = await models.Role.create(editorRole); 157 | let createdEditor2Role = await models.Role.create(editorRole); 158 | let createdAdminRole = await models.Role.create(adminRole); 159 | let createdAdminRole2 = await models.Role.create(adminRole); 160 | 161 | await createdEditor.setRoles(createdEditorRole); 162 | await createdEditor2.setRoles(createdEditor2Role); 163 | await createdAdmin.setRoles(createdAdminRole); 164 | await createdAdmin2.setRoles(createdAdminRole2); 165 | 166 | await models.Bean.create(newBeanBlack); 167 | await models.Bean.create(newBeanWhite); 168 | 169 | let createdPost1 = await models.Post.create(testPost1); 170 | let createdPost2 = await models.Post.create(testPost2); 171 | let createdPost3 = await models.Post.create(testPost3); 172 | 173 | await createdPost1.setCreator(createdEditor); 174 | 175 | let createTag = await models.Tag.create(tag1); 176 | let createTag2 = await models.Tag.create(tag2); 177 | let createTag3 = await models.Tag.create(tag3); 178 | 179 | //await createdPost1.setTags(createTag); 180 | await createdPost1.setTags([createTag,createTag2]); 181 | await createdPost2.setTags([createTag3]); 182 | 183 | 184 | let createdCom1 = await models.Comment.create(Comment1); 185 | let createdCom2 = await models.Comment.create(Comment2); 186 | let createdCom3 = await models.Comment.create(Comment3); 187 | 188 | 189 | } catch (e) { 190 | 191 | console.log("error", e); 192 | 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /server/config/all.json: -------------------------------------------------------------------------------- 1 | { 2 | "locales": ["en", "fr"] 3 | } 4 | -------------------------------------------------------------------------------- /server/config/development.js: -------------------------------------------------------------------------------- 1 | export default { 2 | port: parseInt(process.env.PORT, 10) || 3000, 3 | 'connection': { 4 | 'dialect': 'sqlite', 5 | 'storage': './db.development.sqlite', 6 | 'username': null, 7 | 'password': null, 8 | 'database': null, 9 | 'force': true 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /server/config/init.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import objectAssign from 'react/lib/Object.assign'; 3 | 4 | import baseConfig from './all.json'; 5 | const env = process.env.NODE_ENV || 'development'; 6 | let config; 7 | 8 | try { 9 | config = require(`./${env}`); 10 | 11 | let port = parseInt(config.port, 10) + 2 || '8080'; 12 | let domain = config.domain || 'localhost'; 13 | 14 | config.baseUrl = `http://${domain}:${port}/`; 15 | } 16 | catch (error) { 17 | debug('dev')(`No specific configuration for env ${env}`); 18 | } 19 | 20 | export default objectAssign(baseConfig, config); 21 | -------------------------------------------------------------------------------- /server/config/production.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'port': 3010, 3 | 'domain': 'localhost', 4 | 'connection': { 5 | 'username': 'root', 6 | 'password': null, 7 | 'database': 'mobious', 8 | 'host': '127.0.0.1', 9 | 'dialect': 'mysql', 10 | 'force': false 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /server/config/test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'port': 3000, 3 | 'connection': { 4 | 'dialect': 'sqlite', 5 | 'storage': './db.development.sqlite', 6 | 'username': null, 7 | 'password': null, 8 | 'database': null, 9 | 'force': true 10 | 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /server/controllers/auth.js: -------------------------------------------------------------------------------- 1 | exports.status = function *() { 2 | let authStatus = services.user.getAuthStatus(this); 3 | this.body = authStatus 4 | }; 5 | -------------------------------------------------------------------------------- /server/controllers/bean.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | exports.index = function *() { 4 | 5 | let beans = yield models.Bean.findAll() 6 | 7 | this.body = {beans} 8 | }; 9 | 10 | exports.get = function *() { 11 | 12 | let beanId = this.params.id; 13 | 14 | let bean = yield models.Bean.findOne(userId); 15 | 16 | this.body = {bean} 17 | }; 18 | 19 | 20 | exports.create = function *() { 21 | 22 | let newBean = this.request.body; 23 | 24 | let result = null; 25 | 26 | try { 27 | result = yield models.User.create(newBean); 28 | } catch (e) { 29 | console.error("create user error", e); 30 | } 31 | 32 | let bean = result; 33 | 34 | this.body = {bean} 35 | }; 36 | 37 | 38 | exports.delete = function *() { 39 | 40 | let beanId = this.params.id; 41 | 42 | let result = null; 43 | 44 | try { 45 | let bean = yield models.Bean.findById(beanId); 46 | result = bean.destroy() 47 | } catch (e) { 48 | console.error("delete user error", e); 49 | } 50 | 51 | this.body = {result} 52 | }; 53 | -------------------------------------------------------------------------------- /server/controllers/comment.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | exports.index = function *() { 4 | 5 | let comments = yield models.Comment.findAll(); 6 | 7 | this.body = {comments} 8 | }; 9 | 10 | exports.get = function *() { 11 | 12 | let commentId = this.params.id; 13 | 14 | let comment = yield models.Comment.findById(commentId); 15 | 16 | this.body = {comment} 17 | }; 18 | 19 | 20 | exports.create = function *() { 21 | 22 | let newComment = this.request.body; 23 | 24 | let result = null; 25 | 26 | try { 27 | result = yield models.Comment.create(newComment); 28 | } catch (e) { 29 | console.error("create comment error", e); 30 | } 31 | 32 | let comment = result; 33 | 34 | this.body = {comment} 35 | }; 36 | 37 | 38 | exports.delete = function *() { 39 | 40 | let commentId = this.params.id; 41 | 42 | let result = null; 43 | 44 | try { 45 | let comment = yield models.Comment.findById(commentId); 46 | result = comment.destroy() 47 | } catch (e) { 48 | console.error("delete comment error", e); 49 | } 50 | 51 | this.body = {result} 52 | }; 53 | -------------------------------------------------------------------------------- /server/controllers/index.js: -------------------------------------------------------------------------------- 1 | import UserController from './user'; 2 | import BeanController from './bean'; 3 | import PostController from './post'; 4 | import AuthController from './auth'; 5 | import CommentController from './comment'; 6 | import RoleController from './role'; 7 | 8 | import Router from 'koa-router'; 9 | import fs from 'fs'; 10 | import path from 'path'; 11 | 12 | 13 | export default class Routes { 14 | 15 | constructor (app, passport) { 16 | var router = new Router(); 17 | this.router = router; 18 | this.app = app; 19 | this.passport = passport; 20 | 21 | } 22 | 23 | setupPublicRoute() { 24 | var app = this.app; 25 | var passport = this.passport; 26 | 27 | let assets; 28 | if (process.env.NODE_ENV === 'development') { 29 | assets = fs.readFileSync(path.resolve(__dirname, '../webpack-stats.json')); 30 | assets = JSON.parse(assets); 31 | } 32 | else { 33 | assets = require('../webpack-stats.json'); 34 | } 35 | 36 | var publicRoute = new Router() 37 | 38 | publicRoute.get('/auth/login', function*() { 39 | this.render('login', {assets}) 40 | }) 41 | 42 | publicRoute.post('/auth/login', function*(next) { 43 | let ctx = this; 44 | yield passport.authenticate('local', function*(err, user, info) { 45 | if (err) throw err 46 | if (user === false) { 47 | ctx.status = 401 48 | ctx.body = { loginMessage: 'fail' } 49 | }else if (user.activated === false) { 50 | ctx.status = 401 51 | ctx.body = { loginMessage: 'activated' } 52 | } else { 53 | console.log('OPPPPPPS'); 54 | yield ctx.logIn(user); 55 | let authStatus = services.user.getAuthStatus(ctx); 56 | ctx.body = { authStatus: authStatus, loginMessage: 'success' } 57 | } 58 | }).call(this, next) 59 | }); 60 | 61 | 62 | publicRoute.get('/auth/status', AuthController.status); 63 | 64 | publicRoute.get('/logout', function*(next) { 65 | this.logout() 66 | this.redirect('/') 67 | }) 68 | 69 | publicRoute.get('/auth/facebook', 70 | passport.authenticate('facebook') 71 | ) 72 | 73 | publicRoute.get('/auth/facebook/callback', 74 | passport.authenticate('facebook', { 75 | successRedirect: '/', 76 | failureRedirect: '/auth/login' 77 | }) 78 | ) 79 | 80 | publicRoute.get('/rest/user/:id', UserController.get); 81 | publicRoute.get('/rest/user/', UserController.index); 82 | publicRoute.get('/rest/bean/', BeanController.index); 83 | publicRoute.get('/rest/post/', PostController.index); 84 | publicRoute.get('/rest/comment/', CommentController.index); 85 | publicRoute.get('/rest/post/:id', PostController.get); 86 | publicRoute.get('/rest/auth/status', AuthController.status); 87 | publicRoute.post('/rest/post/', PostController.create); 88 | publicRoute.post('/rest/post/fileUpload/', PostController.upload); 89 | publicRoute.post('/rest/comment/', CommentController.create); 90 | publicRoute.get('/rest/role/:attribute', RoleController.get); 91 | 92 | 93 | app.use(publicRoute.middleware()) 94 | 95 | var that = this; 96 | 97 | app.use(function*(next) { 98 | if (true || services.user.isAuthenticated(this)) { 99 | yield next 100 | } else { 101 | this.redirect('/auth/login') 102 | } 103 | }) 104 | 105 | } 106 | 107 | setupAppRoute() { 108 | 109 | this.router.post('/rest/user/', UserController.create); 110 | this.router.post('/rest/role/', RoleController.create); 111 | this.router.put('/rest/post/updateEditor/:id', PostController.updateEditor); 112 | this.router.put('/rest/post/:id', PostController.update); 113 | this.router.put('/rest/user/activated/:id', UserController.updateActivated); 114 | this.router.put('/rest/user/resetPasswordByAdmin/:id', UserController.resetPasswordByAdmin); 115 | this.router.delete('/rest/user/:id', UserController.delete); 116 | this.router.delete('/rest/post/:id', PostController.delete); 117 | 118 | this.app.use(this.router.middleware()) 119 | 120 | 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /server/controllers/post.js: -------------------------------------------------------------------------------- 1 | import parse from 'co-busboy'; 2 | import fs from 'fs-extra'; 3 | var os = require('os'); 4 | var path = require('path'); 5 | 6 | exports.index = function *() { 7 | 8 | let posts = yield models.Post.findAll() 9 | 10 | this.body = {posts} 11 | }; 12 | 13 | exports.get = function *() { 14 | 15 | let postId = this.params.id; 16 | let post = yield models.Post.find({ 17 | where: { 18 | id: postId 19 | }, 20 | include: [ { model: models.Tag ,attributes: ['name']} ] 21 | }); 22 | 23 | this.body = {post} 24 | }; 25 | 26 | 27 | exports.create = function *() { 28 | 29 | try { 30 | 31 | let tmpPost = this.request.body; 32 | let UserId = yield services.user.getSessionUser(this) 33 | let post_data = { 34 | title: tmpPost.title, 35 | content: tmpPost.content, 36 | img: tmpPost.img, 37 | CreatorId: UserId.id, 38 | EditorId: UserId.id 39 | } 40 | let post = null; 41 | let tag_arr = []; 42 | let tmpTag = []; 43 | 44 | let result = yield models.Post.create(post_data); 45 | let PostId = result.id; 46 | 47 | yield tmpPost.tags.map((tag) => { 48 | tmpTag.push({ 49 | name: tag, 50 | PostId: PostId 51 | }); 52 | }); 53 | 54 | let tagResult = yield models.Tag.bulkCreate(tmpTag); 55 | tagResult.forEach((tr) => { 56 | tag_arr.push(tr.name); 57 | }); 58 | 59 | post = result; 60 | post.setDataValue('tags', tag_arr); 61 | 62 | this.body = {post}; 63 | 64 | } catch (error) { 65 | console.log(error.stack); 66 | this.body = {post, error}; 67 | } 68 | }; 69 | 70 | exports.update = function *() { 71 | 72 | 73 | try { 74 | let postId = this.params.id; 75 | let editPost = this.request.body; 76 | let UserId = services.user.getSessionUser(this).id; 77 | let result = null; 78 | 79 | let post = yield models.Post.find({ 80 | where: { 81 | id: postId 82 | }, 83 | include: [ { model: models.Tag } ] 84 | }); 85 | if( UserId === post.CreatorId || UserId === post.EditorId ) 86 | { 87 | // Remove Tag 88 | 89 | // yield * foreach(post.Tags, function * (tag, index) { 90 | 91 | yield post.Tags.map((tag) => { 92 | let state = editPost.tags.indexOf(tag.name); 93 | if(state === -1){ 94 | // New Post Data not have this tag, Remove Tag. 95 | models.Tag.destroy({ 96 | where:{ 97 | id:tag.id 98 | } 99 | }); 100 | }else { 101 | // Is exist remove in editTag 102 | editPost.tags.splice(state,1); 103 | } 104 | }); 105 | 106 | yield editPost.tags.map((tag) => { 107 | // Create new Tag 108 | models.Tag.create({ 109 | name:tag, 110 | PostId:post.id 111 | }); 112 | }); 113 | 114 | // Post 115 | post.title=editPost.title; 116 | post.content=editPost.content; 117 | post.img=editPost.img; 118 | 119 | result = yield post.save(); 120 | 121 | this.body = {result}; 122 | } 123 | 124 | } catch (error) { 125 | console.log(error.stack); 126 | this.body = {result, error}; 127 | } 128 | 129 | 130 | }; 131 | 132 | exports.updateEditor = function *() { 133 | 134 | try { 135 | let authStatus = services.user.getAuthStatus(this); 136 | if(authStatus.authority ==='admin'){ 137 | let postId = this.params.id; 138 | let editorId = this.request.body.editorId; 139 | let result = null; 140 | 141 | if(editorId==='0'){ 142 | editorId=null; 143 | }; 144 | 145 | let post = yield models.Post.findById(postId); 146 | post.EditorId = editorId; 147 | yield post.save(); 148 | 149 | this.body = {post}; 150 | } 151 | } catch (error) { 152 | console.log(error.stack); 153 | this.body = {result, error}; 154 | } 155 | 156 | }; 157 | 158 | exports.upload = function* (next) { 159 | 160 | // ignore non-POSTs 161 | if ('POST' != this.method) return yield next; 162 | 163 | try { 164 | // multipart upload 165 | let parts = parse(this, { 166 | autoFields: true 167 | }); 168 | let part; 169 | let dir = '.tmp/images/post/'; 170 | fs.ensureDirSync(dir); 171 | let filename = Math.floor(Math.random()*1000000) + '.png'; 172 | 173 | while (part = yield parts) { 174 | var stream = fs.createWriteStream(path.join(dir, filename)); 175 | part.pipe(stream); 176 | } 177 | console.log('uploading %s -> %s', filename, stream.path); 178 | this.body = {success: true, filename: filename} 179 | 180 | } catch (e) { 181 | 182 | console.log(e.stack); 183 | this.body = {success: false}; 184 | 185 | } 186 | }; 187 | 188 | 189 | 190 | exports.delete = function *() { 191 | 192 | 193 | 194 | let postId = this.params.id; 195 | 196 | let result = null; 197 | 198 | try { 199 | let UserId = services.user.getSessionUser(this).id; 200 | let post = yield models.Post.findById(postId); 201 | 202 | if( UserId === post.CreatorId || UserId === post.EditorId ) 203 | result = yield post.destroy() 204 | 205 | } catch (e) { 206 | console.error("delete post error", e); 207 | } 208 | 209 | this.body = {result} 210 | }; 211 | -------------------------------------------------------------------------------- /server/controllers/role.js: -------------------------------------------------------------------------------- 1 | exports.get = function *() { 2 | 3 | let attribute = this.params.attribute; 4 | 5 | let attributes = yield models.User.findAll({ 6 | attributes:['id','username'], 7 | include: [ { model: models.Role, where:{authority:attribute}} ] 8 | }); 9 | 10 | this.body = {attributes} 11 | }; 12 | 13 | 14 | exports.create = function *() { 15 | let newRole = this.request.body; 16 | 17 | let result = null; 18 | 19 | try { 20 | result = yield models.Role.create(newRole); 21 | } catch (e) { 22 | console.error("create user error", e); 23 | } 24 | 25 | let role = result; 26 | 27 | this.body = {role} 28 | }; 29 | -------------------------------------------------------------------------------- /server/controllers/user.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | exports.index = function *() { 4 | 5 | let users = yield models.User.findAll() 6 | 7 | this.body = {users} 8 | }; 9 | 10 | exports.get = function *() { 11 | 12 | let userId = this.params.id; 13 | 14 | let user = yield models.User.findById(userId); 15 | 16 | this.body = {user} 17 | }; 18 | 19 | 20 | exports.create = function *() { 21 | 22 | let newUser = this.request.body; 23 | 24 | let result = null; 25 | 26 | try { 27 | result = yield models.User.create(newUser); 28 | } catch (e) { 29 | console.error("create user error", e); 30 | } 31 | 32 | let user = result; 33 | 34 | this.body = {user} 35 | }; 36 | 37 | 38 | exports.delete = function *() { 39 | 40 | let userId = this.params.id; 41 | 42 | let result = null; 43 | 44 | try { 45 | let user = yield models.User.findById(userId); 46 | result = yield user.destroy() 47 | } catch (e) { 48 | console.error("delete user error", e); 49 | } 50 | 51 | this.body = {result} 52 | }; 53 | 54 | 55 | exports.updateActivated = function *() { 56 | 57 | let UserId = services.user.getSessionUser(this).id; 58 | 59 | let editUserId = this.params.id; 60 | let editActivated = this.request.body.activated; 61 | 62 | let result = null; 63 | 64 | try { 65 | let user = yield models.User.findById(editUserId); 66 | 67 | //Confirm not myself account 68 | if(UserId !== user.id){ 69 | user.activated = editActivated; 70 | result = yield user.save(); 71 | } 72 | result = user; 73 | } catch (e) { 74 | console.error("create user error", e); 75 | } 76 | 77 | let user = result; 78 | 79 | this.body = {user} 80 | }; 81 | 82 | exports.resetPasswordByAdmin= function *() { 83 | 84 | let result = false; 85 | 86 | try { 87 | 88 | let authStatus = services.user.getAuthStatus(this); 89 | 90 | //Verify Role is Admin 91 | if(authStatus.authority ==='admin'){ 92 | let userId = this.params.id; 93 | let pwd1 = this.request.body.pwd1; 94 | let pwd2 = this.request.body.pwd2; 95 | 96 | //Check Password 97 | if(pwd1 === pwd2){ 98 | let user = yield models.User.findById(userId); 99 | user.password = pwd1; 100 | yield user.save(); 101 | result = true; 102 | } 103 | 104 | } 105 | 106 | } catch (e) { 107 | console.error("reset password error", e); 108 | } 109 | this.body = {success:result} 110 | }; 111 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Delete the `BROWSER` env variable if it's present 2 | // https://github.com/iam4x/isomorphic-flux-boilerplate/issues/16 3 | delete process.env.BROWSER; 4 | 5 | // Tell `require` calls to look into `/app` also 6 | // it will avoid `../../../../../` require strings 7 | process.env.NODE_PATH = 'app'; 8 | require('module').Module._initPaths(); 9 | 10 | // Install `babel` hook for ES6 11 | require('babel/register'); 12 | 13 | // Load Intl polyfill 14 | require('utils/intl-polyfill')(require('./config/init').locales); 15 | 16 | // Start the server 17 | module.exports = require('./koa.js'); 18 | -------------------------------------------------------------------------------- /server/koa.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import debug from 'debug'; 5 | 6 | import koa from 'koa'; 7 | import jade from 'koa-jade'; 8 | import mount from 'koa-mount'; 9 | import serve from 'koa-static'; 10 | import helmet from 'koa-helmet'; 11 | import logger from 'koa-logger'; 12 | import favicon from 'koa-favicon'; 13 | import staticCache from 'koa-static-cache'; 14 | import responseTime from 'koa-response-time'; 15 | import koaBodyParser from 'koa-bodyparser'; 16 | 17 | import cors from 'koa-cors'; 18 | 19 | import isomorphicRouter from './router'; 20 | import bootstrap from './bootstrap'; 21 | 22 | import config from './config/init'; 23 | 24 | 25 | import Models from './models'; 26 | import Controllers from './controllers'; 27 | import Services from './services'; 28 | 29 | 30 | 31 | const env = process.env.NODE_ENV || 'development'; 32 | const app = koa(); 33 | 34 | 35 | app.use(cors()); 36 | app.use(koaBodyParser()); 37 | 38 | 39 | // sessions 40 | var session = require('koa-generic-session') 41 | app.keys = ['your-session-secret'] 42 | app.use(session()) 43 | 44 | 45 | // authentication 46 | require('./auth') 47 | var passport = require('koa-passport') 48 | app.use(passport.initialize()) 49 | app.use(passport.session()) 50 | 51 | 52 | 53 | 54 | // setup rest models 55 | global.models = (new Models()).getDb(); 56 | 57 | // add header `X-Response-Time` 58 | app.use(responseTime()); 59 | app.use(logger()); 60 | 61 | // various security headers 62 | app.use(helmet()); 63 | 64 | if (env === 'production') { 65 | // set debug env to `koa` only 66 | // must be set programmaticaly for windows 67 | debug.enable('koa'); 68 | 69 | // load production middleware 70 | app.use(require('koa-conditional-get')()); 71 | app.use(require('koa-etag')()); 72 | app.use(require('koa-compressor')()); 73 | 74 | } 75 | 76 | if (env === 'development') { 77 | // set debug env, must be programmaticaly for windows 78 | debug.enable('dev,koa'); 79 | // log when process is blocked 80 | require('blocked')((ms) => debug('koa')(`blocked for ${ms}ms`)); 81 | } 82 | 83 | app.use(favicon(path.join(__dirname, '../app/images/favicon.ico'))); 84 | 85 | app.use(jade.middleware({ 86 | debug: true, 87 | pretty: true, 88 | viewPath: path.join(__dirname, '/views') 89 | })); 90 | 91 | 92 | // setup rest router 93 | var services = new Services(); 94 | 95 | global.services = services; 96 | 97 | var controllers = new Controllers(app, passport); 98 | controllers.setupPublicRoute() 99 | controllers.setupAppRoute() 100 | 101 | 102 | 103 | 104 | const cacheOpts: Object = {maxAge: 86400000, gzip: true}; 105 | 106 | // Proxy asset folder to webpack development server in development mode 107 | app.use(mount('/alloyeditor', staticCache(path.join(__dirname, '../node_modules/alloyeditor/dist/alloy-editor'), cacheOpts))); 108 | app.use(mount('/assets', serve(path.join(__dirname, '../.tmp'), cacheOpts))); 109 | 110 | 111 | if (env === 'development') { 112 | var webpackConfig: Object = require('./../webpack/dev.config'); 113 | app.use(mount('/assets', require('koa-proxy')({ host: `http://localhost:${webpackConfig.server.port}` }))); 114 | } 115 | else { 116 | app.use(mount('/assets', staticCache(path.join(__dirname, '../dist'), cacheOpts))); 117 | } 118 | 119 | 120 | 121 | app.use(isomorphicRouter); 122 | 123 | 124 | 125 | var liftApp = async () => { 126 | 127 | 128 | await models.sequelize.sync({force: config.connection.force}) 129 | 130 | await bootstrap(); 131 | app.listen(config.port); 132 | 133 | if (process.send) process.send('online'); 134 | debug('koa')(`Application started on port ${config.port}`); 135 | 136 | return app; 137 | 138 | } 139 | 140 | if (env !== 'test') liftApp(); 141 | 142 | module.exports = liftApp 143 | -------------------------------------------------------------------------------- /server/models/bean.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var Bean = sequelize.define('Bean', { 5 | name: DataTypes.STRING, 6 | place: DataTypes.STRING, 7 | breed: DataTypes.STRING, 8 | manor: DataTypes.STRING, 9 | method: DataTypes.STRING, 10 | roastDate: DataTypes.STRING, 11 | roastType: DataTypes.STRING, 12 | flavour: DataTypes.STRING, 13 | lat: DataTypes.FLOAT, 14 | lon: DataTypes.FLOAT, 15 | coverUrl: DataTypes.STRING, 16 | thumbCoverUrl: DataTypes.STRING 17 | }, { 18 | classMethods: { 19 | associate: (models) => { 20 | Bean.hasOne(models.User); 21 | } 22 | } 23 | }); 24 | 25 | return Bean; 26 | }; 27 | -------------------------------------------------------------------------------- /server/models/comment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var Comment = sequelize.define('Comment', { 5 | author: DataTypes.STRING, 6 | content: DataTypes.STRING 7 | }, { 8 | classMethods: { 9 | associate: (models) => { 10 | } 11 | } 12 | }); 13 | 14 | return Comment; 15 | }; 16 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | import {connection} from '../config/init'; 4 | 5 | var fs = require("fs"); 6 | var path = require("path"); 7 | var Sequelize = require("sequelize"); 8 | 9 | export default class Models { 10 | 11 | constructor() { 12 | var sequelize = new Sequelize(connection.database, connection.username, connection.password, connection); 13 | 14 | var db = {}; 15 | 16 | fs 17 | .readdirSync(__dirname) 18 | .filter((file) => { 19 | return (file.indexOf('.') !== 0) && (file !== 'index.js'); 20 | }) 21 | .forEach((file) => { 22 | var model = sequelize.import(path.join(__dirname, file)); 23 | db[model.name] = model; 24 | }); 25 | 26 | Object.keys(db).forEach((modelName) => { 27 | if ('associate' in db[modelName]) { 28 | db[modelName].associate(db); 29 | } 30 | }); 31 | 32 | this.db = db; 33 | this.db.sequelize = sequelize; 34 | 35 | } 36 | 37 | getDb() { 38 | return this.db; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /server/models/post.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var Post = sequelize.define('Post', { 5 | title: DataTypes.STRING, 6 | content: DataTypes.STRING, 7 | img: DataTypes.STRING 8 | }, { 9 | classMethods: { 10 | associate: (models) => { 11 | Post.belongsTo(models.User, {as: 'Creator'} ), 12 | Post.belongsTo(models.User, {as: 'Editor'} ), 13 | Post.hasMany(models.Tag); 14 | } 15 | }, 16 | paranoid: true 17 | }); 18 | 19 | return Post; 20 | }; 21 | -------------------------------------------------------------------------------- /server/models/role.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | var Role = sequelize.define('Role', { 3 | authority: DataTypes.STRING 4 | }, { 5 | classMethods: { 6 | associate: (models) => { 7 | Role.belongsTo(models.User); 8 | } 9 | } 10 | }); 11 | 12 | return Role; 13 | }; 14 | -------------------------------------------------------------------------------- /server/models/tag.js: -------------------------------------------------------------------------------- 1 | module.exports = (sequelize, DataTypes) => { 2 | var Tag = sequelize.define('Tag', { 3 | name: DataTypes.STRING 4 | }, { 5 | classMethods: { 6 | associate: (models) => { 7 | Tag.belongsTo(models.Post); 8 | } 9 | } 10 | }); 11 | 12 | return Tag; 13 | }; 14 | -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (sequelize, DataTypes) => { 4 | var User = sequelize.define('User', { 5 | username: DataTypes.STRING, 6 | gender: DataTypes.STRING, 7 | email: DataTypes.STRING, 8 | password: DataTypes.STRING, 9 | phone: DataTypes.STRING, 10 | cell: DataTypes.STRING, 11 | picture: DataTypes.TEXT, 12 | activated: DataTypes.BOOLEAN 13 | }, { 14 | classMethods: { 15 | associate: (models) => { 16 | User.hasMany(models.Post); 17 | User.hasMany(models.Role); 18 | } 19 | } 20 | }); 21 | 22 | return User; 23 | }; 24 | -------------------------------------------------------------------------------- /server/router.jsx: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | 3 | import React from 'react'; 4 | import Router from 'react-router'; 5 | import Location from 'react-router/lib/Location'; 6 | 7 | // Paths are relative to `app` directory 8 | import routes from 'routes'; 9 | import Flux from 'utils/flux'; 10 | 11 | // We need wrap `Router.run` into a promise 12 | // in order to use the keyword `yield` and keep 13 | // the correct way `koajs` works 14 | const promisifiedRouter = (customRoutes, location) => { 15 | return new Promise((resolve) => { 16 | Router.run(customRoutes, location, (error, initialState, transition) => 17 | resolve({error, initialState, transition}) 18 | ); 19 | }); 20 | }; 21 | 22 | export default function *(next) { 23 | 24 | var url = this.request.url; 25 | 26 | if (url.startsWith("/rest") || url.startsWith("/auth")){ 27 | return yield next; 28 | } 29 | 30 | // Init alt instance 31 | const flux = new Flux(); 32 | 33 | // Get request locale for rendering 34 | const locale = this.cookies.get('_lang') || this.acceptsLanguages(require('./config/init').locales) || 'en'; 35 | const {messages} = require(`data/${locale}`); 36 | 37 | // Populate store with locale 38 | 39 | let authStatus = services.user.getAuthStatus(this); 40 | global.flux = flux; 41 | flux 42 | .getActions('locale') 43 | .switchLocaleSuccess({locale, messages}); 44 | flux 45 | .getActions('auth') 46 | .fetchStatusSuccess(authStatus); 47 | 48 | debug('dev')(`locale of request: ${locale}`); 49 | 50 | try { 51 | // Pass correct location of the request to `react-router` 52 | // it will return the matched components for the route into `initialState` 53 | const location = new Location(this.request.path, this.request.query); 54 | const {error, initialState, transition} = yield promisifiedRouter(routes, location); 55 | 56 | // Render 500 error page from server 57 | if (error) throw error; 58 | 59 | const {isCancelled, redirectInfo} = transition; 60 | if (isCancelled) return this.redirect(redirectInfo.pathname); 61 | 62 | // Render application of correct location 63 | // We need to re-define `createElement` of `react-router` 64 | // in order to include `flux` on children components props 65 | const routerProps = Object.assign({}, initialState, 66 | { 67 | location, 68 | createElement: (component, props) => { 69 | // Take locale and messages from `locale` store 70 | // and pass them to every components rendered from `Router` 71 | const i18n = flux.getStore('locale').getState(); 72 | return React.createElement( 73 | component, 74 | Object.assign(props, {flux, ...i18n}) 75 | ); 76 | } 77 | } 78 | ); 79 | 80 | // Use `alt-resolver` to render component with fetched data 81 | const {body, title} = yield flux.render(); 82 | 83 | // Assets name are found into `webpack-stats` 84 | const assets = require('./webpack-stats.json'); 85 | 86 | // Don't cache assets name on dev 87 | if (process.env.NODE_ENV === 'development') { 88 | delete require.cache[require.resolve('./webpack-stats.json')]; 89 | } 90 | 91 | debug('dev')('return html content'); 92 | this.render('main', {body, assets, locale, title}); 93 | } 94 | // Catch error from rendering procress 95 | catch (error) { 96 | // If the error got a `redirect` key 97 | // we should trigger a redirection from 98 | // the server to keep things isomorphic 99 | if (error.redirect) { 100 | return this.redirect(error.redirect); 101 | } 102 | 103 | // In other cases just return the error 104 | throw error; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /server/services/index.js: -------------------------------------------------------------------------------- 1 | 2 | import UserService from './user'; 3 | 4 | 5 | export default class Services { 6 | 7 | constructor () { 8 | this.user = new UserService(); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /server/services/user.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | export default class User { 3 | 4 | constructor (app, passport) { 5 | this.isAuthenticated = (app) => { 6 | return app.isAuthenticated(); 7 | } 8 | 9 | this.getSessionUser = (app) => { 10 | return app.session.passport.user; 11 | } 12 | 13 | this.getAuthStatus = (app) => { 14 | let isAuthenticated = this.isAuthenticated(app); 15 | let sessionUser = this.getSessionUser(app); 16 | let authority = ''; 17 | if (_.has(sessionUser, 'Roles') && sessionUser.Roles.length !== 0) { 18 | authority = sessionUser.Roles[0].authority; 19 | 20 | } 21 | return {isAuthenticated, sessionUser, authority} 22 | } 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /server/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang='#{locale}') 3 | head 4 | //if IE 8 5 | script(src='//cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.1/es5-shim.min.js') 6 | script(src='//cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.1/es5-sham.min.js') 7 | script(src='//cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.2/html5shiv.min.js') 8 | script(src='//cdn.uriit.ru/console-polyfill/index.js') 9 | 10 | meta(charset='utf-8') 11 | link(rel='icon', type='image/ico', href='/favicon.ico') 12 | 13 | each style in assets.style 14 | link(rel='stylesheet', href='#{style}') 15 | 16 | title ISO-ReactJS 17 | 18 | body 19 | block body 20 | 21 | each script in assets.script 22 | script(src='#{script}') 23 | 24 | -------------------------------------------------------------------------------- /server/views/login.jade: -------------------------------------------------------------------------------- 1 | extends ./layout 2 | 3 | 4 | block body 5 | a(href='/auth/facebook') Sign in with Facebook 6 | -------------------------------------------------------------------------------- /server/views/main.jade: -------------------------------------------------------------------------------- 1 | extends ./layout 2 | 3 | 4 | block body 5 | content!= body 6 | -------------------------------------------------------------------------------- /test/server/beforeAll.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import request from 'supertest'; 3 | import sinon from 'sinon'; 4 | import liftApp from '../../server'; 5 | chai.should(); 6 | global.sinon = sinon; 7 | before(async (done) => { 8 | let app = await liftApp(); 9 | global.app = app; 10 | global.request = 11 | request.agent(app.listen()); 12 | done(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/server/controllers/auth.spec.js: -------------------------------------------------------------------------------- 1 | describe('auth', () => { 2 | 3 | describe('local login spec', () => { 4 | 5 | before(async (done) => { 6 | 7 | let testUser = { 8 | 'username': 'test', 9 | 'password': 'test', 10 | 'gender': 'male', 11 | 'email': 'test@test.com', 12 | 'phone': '(951)-385-6121', 13 | 'cell': '(657)-919-3511', 14 | 'picture': '', 15 | activated: true 16 | } 17 | await models.User.create(testUser); 18 | done(); 19 | 20 | }); 21 | 22 | it('do login should be success.', async (done) => { 23 | let loginUserFormData = { 24 | 'username': 'test', 25 | 'password': 'test' 26 | }; 27 | 28 | let loginResult = await new Promise((resolve, reject) => { 29 | request.post('/auth/login') 30 | .send(loginUserFormData) 31 | .expect(200) 32 | .end((error, res) => { 33 | if (error) return reject(error); 34 | return resolve(res.body); 35 | }) 36 | }); 37 | 38 | loginResult.authStatus.isAuthenticated.should.be.true; 39 | let authResult = await new Promise((resolve, reject) => { 40 | request.get('/auth/status') 41 | .expect(200) 42 | .end((error, res) => { 43 | if (error) return reject(error); 44 | return resolve(res.body); 45 | }) 46 | }); 47 | 48 | let sessionUser = authResult.sessionUser; 49 | let isAuthenticated = authResult.isAuthenticated; 50 | 51 | isAuthenticated.should.be.true; 52 | 53 | try { 54 | sessionUser.should.be.Object; 55 | sessionUser.should.have.contain.keys('id', 'username', 'email'); 56 | done(); 57 | } catch (e) { 58 | done(e); 59 | } 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/server/controllers/bean.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Dependencies 4 | */ 5 | 6 | describe("bean", () => { 7 | 8 | before((done)=>{ 9 | sinon.stub(services.user, 'isAuthenticated', (app) =>{ 10 | return true; 11 | }); 12 | done(); 13 | }); 14 | 15 | after((done) =>{ 16 | services.user.isAuthenticated.restore(); 17 | done(); 18 | }); 19 | 20 | it("index all bean", (done) => { 21 | 22 | request.get("/rest/bean/") 23 | .expect(200) 24 | .end((error, res) => { 25 | let beans = res.body.beans; 26 | 27 | beans.should.be.Array; 28 | beans[0].id.should.greaterThan(0); 29 | 30 | done(error); 31 | }); 32 | 33 | }); 34 | 35 | 36 | }); 37 | -------------------------------------------------------------------------------- /test/server/controllers/comment.spec.js: -------------------------------------------------------------------------------- 1 | describe('comment', () => { 2 | describe('index comments', () => { 3 | before(async (done) => { 4 | 5 | let testComment = { 6 | 'author' : 'test comment author', 7 | 'content': 'test index content' 8 | } 9 | 10 | let createdComment = await models.Comment.create(testComment); 11 | done(); 12 | }); 13 | 14 | it("index all comments", async (done) => { 15 | 16 | let indexCommentResult = await new Promise((resolve, reject) => { 17 | request.get("/rest/comment/") 18 | .expect(200) 19 | .end((error, res) => { 20 | if (error) return reject(error); 21 | return resolve(res.body.comments); 22 | }); 23 | }); 24 | let Comments = indexCommentResult; 25 | 26 | try { 27 | Comments.should.be.Array; 28 | Comments[0].id.should.be.greaterThan(0); 29 | Comments[0].should.have.contain.keys('id', 'author', 'content'); 30 | done(); 31 | } catch (e) { 32 | done(e); 33 | } 34 | }); 35 | }); 36 | describe('create comment', () => { 37 | it("create an comment", async (done) => { 38 | 39 | let seedComment = { 40 | 'author' : 'seedAuthor', 41 | 'content': 'seedContent' 42 | }; 43 | 44 | let createdCommentResult = await new Promise((resolve, reject) => { 45 | request.post("/rest/comment/") 46 | .send(seedComment) 47 | .expect(200) 48 | .end((error, res) => { 49 | if (error) return reject(error); 50 | return resolve(res.body.comment); 51 | }); 52 | }); 53 | 54 | let result = await createdCommentResult; 55 | console.log('result', result); 56 | try { 57 | result.should.be.Object; 58 | result.id.should.be.greaterThan(0); 59 | result.author.should.equal(seedComment.author); 60 | done(); 61 | } catch (e) { 62 | done(e); 63 | } 64 | 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /test/server/controllers/post.spec.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | 3 | describe("post", () => { 4 | before((done)=>{ 5 | sinon.stub(services.user, 'isAuthenticated', (app) =>{ 6 | return true; 7 | }); 8 | 9 | sinon.stub(services.user, 'getSessionUser', (app) =>{ 10 | return {id: 2}; 11 | }); 12 | done(); 13 | }); 14 | 15 | after((done) =>{ 16 | services.user.isAuthenticated.restore(); 17 | done(); 18 | }); 19 | 20 | 21 | describe('find one and all', (done) => { 22 | 23 | 24 | it("index all post", (done) => { 25 | 26 | request.get("/rest/post/") 27 | .expect(200) 28 | .end((error, res) => { 29 | let posts = res.body.posts; 30 | 31 | posts.should.be.Array; 32 | posts[0].id.should.greaterThan(0); 33 | 34 | done(error); 35 | }); 36 | 37 | }); 38 | 39 | it("find single post", (done) => { 40 | 41 | request.get("/rest/post/1") 42 | .expect(200) 43 | .end((error, res) => { 44 | 45 | models.User.findById('1').then((result) =>{ 46 | (result !== null).should.true 47 | done(error); 48 | }); 49 | }); 50 | }); 51 | }); 52 | 53 | describe('create a post', () => { 54 | it('create', async (done) => { 55 | 56 | let seedPost = { 57 | title: 'createPostTitle', 58 | content: 'createPostContent', 59 | tags: ['tag1', 'tag2', 'tag3'], 60 | img: 'createPostImgDir' 61 | }; 62 | 63 | let createResult = await new Promise((resolve, reject) => { 64 | request.post('/rest/post/') 65 | .send(seedPost) 66 | .expect(200) 67 | .end((error, res) => { 68 | if (error) return reject(error); 69 | return resolve(res.body.post); 70 | }); 71 | }); 72 | 73 | let result = await createResult; 74 | console.log('spec returns result ', result); 75 | try { 76 | result.title.should.equal(seedPost.title); 77 | result.tags.should.eql(seedPost.tags); 78 | result.id.should.be.greaterThan(0); 79 | done(); 80 | } catch (e) { 81 | done(e); 82 | } 83 | }); 84 | }); 85 | 86 | describe('find one and all', (done) => { 87 | it("file Upload", (done) => { 88 | request.post('/rest/post/fileUpload/') 89 | .attach('file', 'test/server/resources/mobious.png') 90 | .expect(200) 91 | .end(function(err, res) { 92 | done(err) 93 | }); 94 | }); 95 | }); 96 | 97 | it("update post", (done) => { 98 | let updatePost = { 99 | title: '111', 100 | content: 'ssss', 101 | tags: ['aa', 'bb'] 102 | } 103 | request.put("/rest/post/1") 104 | .expect(200) 105 | .send(updatePost) 106 | .end((error, res) => { 107 | 108 | models.Post.find({ 109 | where: { 110 | id: 1 111 | }, 112 | include: [ { model: models.Tag } ] 113 | }).then((updatedPost)=>{ 114 | updatedPost.Tags[0].name.should.be.equal('aa'); 115 | updatedPost.Tags[1].name.should.be.equal('bb'); 116 | done(); 117 | 118 | }); 119 | }); 120 | }); 121 | 122 | it("update post editor", (done) => { 123 | 124 | //Setting is 'admin' authority 125 | sinon.stub(services.user, 'getAuthStatus', (app) =>{ 126 | return {authority: 'admin'}; 127 | }); 128 | 129 | let updateEditorId = { 130 | editorId: 2 131 | } 132 | 133 | request.put("/rest/post/updateEditor/1") 134 | .expect(200) 135 | .send(updateEditorId) 136 | .end((error, res) => { 137 | models.Post.find({ 138 | where: { 139 | id: 1 140 | } 141 | }).then((updatedPost)=>{ 142 | updatedPost.EditorId.should.be.equal(updateEditorId.editorId); 143 | done(); 144 | }); 145 | }); 146 | 147 | 148 | }); 149 | 150 | it("delete post", (done) => { 151 | 152 | let postId = 1; 153 | 154 | request.delete("/rest/post/"+postId) 155 | .expect(200) 156 | .end((error, res) => { 157 | 158 | models.Post.findById(postId).then((result) =>{ 159 | (result == null).should.true 160 | done(error); 161 | }); 162 | 163 | }); 164 | 165 | }); 166 | 167 | }); 168 | -------------------------------------------------------------------------------- /test/server/controllers/user.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * Dependencies 4 | */ 5 | 6 | describe("User", () => { 7 | 8 | before((done)=>{ 9 | sinon.stub(services.user, 'isAuthenticated', (app) =>{ 10 | return true; 11 | }); 12 | 13 | sinon.stub(services.user, 'getSessionUser', (app) =>{ 14 | return {id: 4}; 15 | }); 16 | 17 | done(); 18 | }); 19 | 20 | after((done) =>{ 21 | services.user.isAuthenticated.restore(); 22 | done(); 23 | }); 24 | 25 | it("index all user", (done) => { 26 | 27 | request.get("/rest/user/") 28 | .expect(200) 29 | .end((error, res) => { 30 | console.log("res.body.users", res.body.users); 31 | 32 | res.body.users.should.be.Array; 33 | res.body.users[0].id.should.greaterThan(0); 34 | 35 | done(error); 36 | }); 37 | 38 | }); 39 | 40 | 41 | describe("create and delete", () => { 42 | 43 | let createdUser = null; 44 | 45 | console.log("app.isAuthenticated()", global.app); 46 | 47 | it("create user", (done) => { 48 | 49 | let picture = { 50 | "large":"http://api.randomuser.me/portraits/women/72.jpg", 51 | "medium":"http://api.randomuser.me/portraits/med/women/72.jpg", 52 | "thumbnail":"http://api.randomuser.me/portraits/thumb/women/72.jpg" 53 | } 54 | 55 | let newUserParams = { 56 | "username":"testuser", 57 | "password":"testuser", 58 | "gender":"male", 59 | "email":"testuser@testuser.com", 60 | "phone":"(951)-385-6121", 61 | "cell":"(657)-919-3511", 62 | "picture":JSON.stringify(picture) 63 | } 64 | 65 | 66 | request.post("/rest/user") 67 | .send(newUserParams) 68 | .expect(200) 69 | .end((error, res) => { 70 | console.log("res.body.user", res.body.user); 71 | 72 | res.body.user.should.be.Object; 73 | res.body.user.id.should.greaterThan(0); 74 | res.body.user.username.should.equal(newUserParams.username); 75 | 76 | createdUser = res.body.user; 77 | 78 | done(error); 79 | }); 80 | 81 | }); 82 | 83 | it("delete user", (done) => { 84 | 85 | request.delete("/rest/user/"+ createdUser.id) 86 | .expect(200) 87 | .end((error, res) => { 88 | 89 | models.User.findById(createdUser.id).then((result) =>{ 90 | (result == null).should.true 91 | done(error); 92 | }); 93 | 94 | }); 95 | 96 | }); 97 | 98 | }); 99 | 100 | it("update user activated", (done) => { 101 | 102 | //Setting is 'admin' authority 103 | sinon.stub(services.user, 'getAuthStatus', (app) =>{ 104 | return {authority: 'admin'}; 105 | }); 106 | 107 | let editUserId = 2; 108 | let editActivated = {activated: true}; 109 | 110 | request.put("/rest/user/activated/" + editUserId) 111 | .expect(200) 112 | .send(editActivated) 113 | .end((error, res) => { 114 | models.User.find({ 115 | where: { 116 | id: editUserId 117 | } 118 | }).then((updatedUser)=>{ 119 | updatedUser.activated.should.be.equal(editActivated.activated); 120 | done(); 121 | }); 122 | }); 123 | 124 | 125 | }); 126 | 127 | 128 | it("reset password by admin", (done) => { 129 | 130 | //Setting is 'admin' authority 131 | sinon.stub(services.user, 'getAuthStatus', (app) =>{ 132 | return {authority: 'admin'}; 133 | }); 134 | 135 | let UserId = 2; 136 | let editPassword= {pwd1: 'password', pwd2: 'password'}; 137 | 138 | request.put("/rest/user/resetPasswordByAdmin/" + UserId) 139 | .expect(200) 140 | .send(editPassword) 141 | .end((error, res) => { 142 | models.User.find({ 143 | where: { 144 | id: UserId 145 | } 146 | }).then((updatedUser)=>{ 147 | updatedUser.password.should.be.equal(editPassword.pwd1); 148 | done(); 149 | }); 150 | }); 151 | 152 | }); 153 | 154 | }); 155 | -------------------------------------------------------------------------------- /test/server/mocha.opts: -------------------------------------------------------------------------------- 1 | -t 500000 2 | --recursive test/server/beforeAll.js 3 | --reporter spec 4 | -------------------------------------------------------------------------------- /test/server/resources/mobious.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/exma-square/mobious/32c7a6dab6bf3f062395e9f968a24fb0c194790b/test/server/resources/mobious.png -------------------------------------------------------------------------------- /test/spec/components/app.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import Flux from 'utils/flux'; 3 | 4 | import reactRouterStub from '../../utils/stub-router-context'; 5 | import injectLang from '../../utils/inject-lang'; 6 | 7 | import App from 'components/app'; 8 | 9 | const should = chai.should(); 10 | 11 | describe('App', () => { 12 | let node; 13 | let instance; 14 | let flux; 15 | const TestUtils = React.addons.TestUtils; 16 | 17 | // Inject language 18 | beforeEach(() => { 19 | flux = new Flux(); 20 | injectLang(flux); 21 | }); 22 | 23 | beforeEach(() => { 24 | const Stubbed = reactRouterStub(App, {flux}); 25 | const element = React.createElement(Stubbed); 26 | node = window.document.createElement('div'); 27 | instance = React.render(element, node); 28 | }); 29 | 30 | afterEach(function() { 31 | if (instance) React.unmountComponentAtNode(node); 32 | }); 33 | 34 | it('should render header correctly', () => { 35 | const header = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'header'); 36 | header.length.should.eql(1); 37 | }); 38 | 39 | it('should render logo correctly', () => { 40 | const logo = TestUtils.findRenderedDOMComponentWithClass(instance, 'app-logo'); 41 | should.exist(logo); 42 | }); 43 | 44 | it('should change page title', function() { 45 | flux.getActions('page-title').set('foobar'); 46 | document.title.should.eql('ISO-ReactJS | foobar'); 47 | }); 48 | 49 | it('should handle locale change', function(done) { 50 | const handleChange = function({locales}) { 51 | locales[0].should.eql('fr'); 52 | flux.getStore('locale').getLocale().should.eql('fr'); 53 | flux.getStore('locale').unlisten(handleChange); 54 | return done(); 55 | }; 56 | flux.getStore('locale').listen(handleChange); 57 | flux.getActions('locale').switchLocale('fr'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/spec/components/header.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import Flux from 'utils/flux'; 3 | import objectAssign from 'react/lib/Object.assign'; 4 | 5 | import reactRouterStub from '../../utils/stub-router-context'; 6 | import injectLang from '../../utils/inject-lang'; 7 | 8 | import Header from 'components/header'; 9 | 10 | chai.should(); 11 | 12 | describe('Header', () => { 13 | let node; 14 | let instance; 15 | let flux; 16 | const TestUtils = React.addons.TestUtils; 17 | 18 | beforeEach(() => { 19 | flux = new Flux(); 20 | 21 | const props = objectAssign({flux}, injectLang(flux)); 22 | const Stubbed = reactRouterStub(Header, props); 23 | 24 | node = window.document.createElement('div'); 25 | instance = React.render(React.createElement(Stubbed), node); 26 | }); 27 | 28 | afterEach(() => { 29 | if (instance) React.unmountComponentAtNode(node); 30 | }); 31 | 32 | it('should render lang picker correctly', () => { 33 | const langs = TestUtils.findRenderedDOMComponentWithClass(instance, 'lang--picker'); 34 | langs.props.children.length.should.eql(2); 35 | }); 36 | 37 | it('should handle requests change', function() { 38 | flux.getActions('requests').start(); 39 | const spinner = TestUtils.findRenderedDOMComponentWithClass(instance, 'app--spinner'); 40 | spinner.props.className.indexOf('active').should.not.eql(-1); 41 | 42 | flux.getActions('requests').fail(); 43 | spinner.props.className.indexOf('active').should.eql(-1); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/spec/components/lang-picker.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import Flux from 'utils/flux'; 3 | 4 | import injectLang from '../../utils/inject-lang'; 5 | 6 | import LangPicker from 'components/shared/lang-picker'; 7 | 8 | const should = chai.should(); 9 | 10 | describe('LangPicker', () => { 11 | let node; 12 | let instance; 13 | let flux; 14 | let spy; 15 | const TestUtils = React.addons.TestUtils; 16 | 17 | beforeEach(() => { 18 | flux = new Flux(); 19 | spy = sinon.spy(); 20 | 21 | const props = Object.assign( 22 | {activeLocale: 'en', onChange: spy}, 23 | injectLang(flux) 24 | ); 25 | 26 | const element = React.createElement(LangPicker, props); 27 | 28 | node = window.document.createElement('div'); 29 | instance = React.render(element, node); 30 | }); 31 | 32 | afterEach(function() { 33 | if (instance) React.unmountComponentAtNode(node); 34 | }); 35 | 36 | it('should have en locale active', () => { 37 | const active = TestUtils.findRenderedDOMComponentWithClass(instance, 'active'); 38 | should.exist(active); 39 | active.props.children.should.eql('en'); 40 | }); 41 | 42 | it('should call `onChange` handler', function() { 43 | const locales = TestUtils.scryRenderedDOMComponentsWithTag(instance, 'a'); 44 | locales.length.should.eql(2); 45 | 46 | const inactive = locales.find(l => !l.props.className); 47 | TestUtils.Simulate.click(inactive); 48 | 49 | spy.should.have.been.calledOnce; 50 | spy.should.have.been.calledWith('fr'); 51 | }); 52 | 53 | it('should do nothing on same locale click', () => { 54 | const active = TestUtils.findRenderedDOMComponentWithClass(instance, 'active'); 55 | should.exist(active); 56 | active.props.children.should.eql('en'); 57 | TestUtils.Simulate.click(active); 58 | active.props.className.should.eql('active'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/components/profile.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import objectAssign from 'react/lib/Object.assign'; 3 | import {capitalize} from 'lodash'; 4 | 5 | import reactRouterStub from '../../utils/stub-router-context'; 6 | import injectLang from '../../utils/inject-lang'; 7 | 8 | import Flux from 'utils/flux'; 9 | import Profile from 'userManager/components/profile'; 10 | 11 | // import {users} from 'data/users.json'; 12 | 13 | const should = chai.should(); 14 | const seed = '7729a1ef4ba6ef68'; 15 | 16 | describe.skip('Profile', () => { 17 | let flux; 18 | let node; 19 | let instance; 20 | const T = React.addons.TestUtils; 21 | 22 | beforeEach(() => { 23 | flux = new Flux(); 24 | node = window.document.createElement('div'); 25 | 26 | const props = objectAssign({params: {seed}}, {flux}, injectLang(flux)); 27 | const Stubbed = reactRouterStub(Profile, props); 28 | 29 | instance = React.render(React.createElement(Stubbed), node); 30 | }); 31 | 32 | afterEach(function() { 33 | if (instance) React.unmountComponentAtNode(node); 34 | }); 35 | 36 | it('should render user name after request', (done) => { 37 | const handleChange = () => { 38 | const fullName = T.findRenderedDOMComponentWithTag(instance, 'h2'); 39 | should.exist(fullName); 40 | const user = users.find((u) => u.seed === seed); 41 | should.exist(user.user); 42 | fullName.getDOMNode().textContent.should.eql(`${capitalize(user.user.name.first)} ${capitalize(user.user.name.last)}`); 43 | flux.getStore('users').unlisten(handleChange); 44 | return done(); 45 | }; 46 | flux.getStore('users').listen(handleChange); 47 | }); 48 | 49 | it('should render user picture after request', (done) => { 50 | const handleChange = () => { 51 | const picture = T.findRenderedDOMComponentWithTag(instance, 'img'); 52 | should.exist(picture); 53 | const user = users.find((u) => u.seed === seed); 54 | should.exist(user.user); 55 | picture.getDOMNode().src.should.eql(user.user.picture.medium); 56 | flux.getStore('users').unlisten(handleChange); 57 | return done(); 58 | }; 59 | flux.getStore('users').listen(handleChange); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/spec/components/users.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import Flux from 'utils/flux'; 3 | import objectAssign from 'react/lib/Object.assign'; 4 | 5 | import reactRouterStub from '../../utils/stub-router-context'; 6 | import injectLang from '../../utils/inject-lang'; 7 | 8 | import Users from 'userManager/components/users'; 9 | 10 | const should = chai.should(); 11 | 12 | describe.skip('Users', () => { 13 | let node; 14 | let instance; 15 | let flux; 16 | const TestUtils = React.addons.TestUtils; 17 | 18 | beforeEach(() => { 19 | flux = new Flux(); 20 | 21 | const props = objectAssign({flux}, injectLang(flux)); 22 | const Stubbed = reactRouterStub(Users, props); 23 | 24 | node = window.document.createElement('div'); 25 | instance = React.render(React.createElement(Stubbed), node); 26 | }); 27 | 28 | afterEach(() => { 29 | if (instance) { 30 | React.unmountComponentAtNode(node); 31 | } 32 | }); 33 | 34 | it('should render correctly', () => { 35 | const {messages} = flux.getStore('locale').getState(); 36 | const title = TestUtils.findRenderedDOMComponentWithTag(instance, 'h1'); 37 | title.getDOMNode().textContent.should.eql(messages.users.title); 38 | }); 39 | 40 | it('should render without users', () => { 41 | // Check `
  • ` don't exists 42 | const td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row'); 43 | td.should.be.empty; 44 | }); 45 | 46 | it('should render 10 users after first fetch', (done) => { 47 | const handleChange = () => { 48 | const td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row'); 49 | td.length.should.eql(10); 50 | flux.getStore('users').unlisten(handleChange); 51 | return done(); 52 | }; 53 | flux.getStore('users').listen(handleChange); 54 | }); 55 | 56 | it('should add an user after click on add button', (done) => { 57 | const handleAddChange = () => { 58 | // 11 users after add 59 | let td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row'); 60 | td.length.should.eql(11); 61 | 62 | // clean 63 | flux.getStore('users').unlisten(handleAddChange); 64 | return done(); 65 | }; 66 | 67 | const handleFetchChange = () => { 68 | // 10 users after fetch 69 | let td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row'); 70 | td.length.should.eql(10); 71 | 72 | // clean 73 | flux.getStore('users').unlisten(handleFetchChange); 74 | 75 | // add an user 76 | flux.getStore('users').listen(handleAddChange); 77 | const addButton = TestUtils.findRenderedDOMComponentWithClass(instance, 'add--button'); 78 | should.exist(addButton); 79 | 80 | setTimeout(() => { 81 | TestUtils.Simulate.click(addButton); 82 | }, 0); 83 | }; 84 | 85 | flux.getStore('users').listen(handleFetchChange); 86 | }); 87 | 88 | it('should remove an user', (done) => { 89 | const handleChange = () => { 90 | // 10 users after fetch 91 | let td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row'); 92 | td.length.should.eql(10); 93 | 94 | // remove an user 95 | const removeButton = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--remove')[0]; 96 | should.exist(removeButton); 97 | 98 | // wait for dispatch to be done before 99 | // calling another action 100 | setTimeout(() => { 101 | TestUtils.Simulate.click(removeButton); 102 | 103 | // it should have 9 users 104 | td = TestUtils.scryRenderedDOMComponentsWithClass(instance, 'user--row'); 105 | td.length.should.eql(9); 106 | 107 | // clean 108 | flux.getStore('users').unlisten(handleChange); 109 | return done(); 110 | }, 0); 111 | }; 112 | flux.getStore('users').listen(handleChange); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /test/spec/pages/not-found.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | import NotFound from 'pages/not-found'; 4 | 5 | chai.should(); 6 | 7 | describe('NotFoundPage', () => { 8 | let instance; 9 | let node; 10 | const TestUtils = React.addons.TestUtils; 11 | 12 | beforeEach(() => { 13 | node = window.document.createElement('div'); 14 | instance = React.render(, node); 15 | }); 16 | 17 | afterEach(() => { 18 | if (instance) React.unmountComponentAtNode(node); 19 | }); 20 | 21 | it('should render correctly', () => { 22 | const title = TestUtils.findRenderedDOMComponentWithTag(instance, 'h1'); 23 | title.getDOMNode().textContent.should.eql('404'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/pages/server-error.test.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | 3 | import ServerError from 'pages/server-error'; 4 | 5 | chai.should(); 6 | 7 | describe('ErrorPage', () => { 8 | let instance; 9 | let node; 10 | const TestUtils = React.addons.TestUtils; 11 | 12 | beforeEach(() => { 13 | node = window.document.createElement('div'); 14 | instance = React.render(, node); 15 | }); 16 | 17 | afterEach(() => { 18 | if (instance) React.unmountComponentAtNode(node); 19 | }); 20 | 21 | it('should render correctly', () => { 22 | const title = TestUtils.findRenderedDOMComponentWithTag(instance, 'h1'); 23 | title.getDOMNode().textContent.should.eql('500'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/stores/users.test.js: -------------------------------------------------------------------------------- 1 | import Flux from 'utils/flux'; 2 | 3 | chai.should(); 4 | 5 | describe('UsersStore', () => { 6 | let flux; 7 | let store; 8 | let actions; 9 | 10 | beforeEach(() => { 11 | flux = new Flux(); 12 | store = flux.getStore('users'); 13 | actions = flux.getActions('users'); 14 | }); 15 | 16 | it('should overide users on new fetch', (done) => { 17 | let count = 0; 18 | const handleChange = () => { 19 | let {users} = store.getState(); 20 | users.length.should.be.eql(10); 21 | 22 | setTimeout(() => { 23 | if (++count === 2) { 24 | // clean 25 | store.unlisten(handleChange); 26 | return done(); 27 | } 28 | actions.fetch(); 29 | }); 30 | }; 31 | store.listen(handleChange); 32 | actions.fetch(); 33 | }); 34 | 35 | it('should merge users when on new fetch', function(done) { 36 | // add dummy user in store 37 | actions.fetchBySeedSuccess({name: 'foo', seed: 'bar'}); 38 | 39 | const handleChange = function({users}) { 40 | users.length.should.be.eql(11); 41 | store.unlisten(handleChange); 42 | return done(); 43 | }; 44 | 45 | store.listen(handleChange); 46 | actions.fetch(); 47 | }); 48 | 49 | it('should update user with same seed', function() { 50 | actions.fetchBySeedSuccess({name: 'foo', seed: 'bar'}); 51 | 52 | store.getState().users[0].should.eql({name: 'foo', seed: 'bar'}); 53 | 54 | actions.fetchBySeedSuccess({name: 'yolo', seed: 'bar'}); 55 | store.getState().users[0].should.eql({name: 'yolo', seed: 'bar'}); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/spec/utils/alt-resolver.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Flux from 'utils/flux'; 3 | import AltResolver from 'utils/alt-resolver'; 4 | 5 | import injectLang from '../../utils/inject-lang'; 6 | 7 | const should = chai.should(); 8 | 9 | const Dummy = React.createClass({ 10 | render() { 11 | return React.createElement('h1'); 12 | } 13 | }); 14 | 15 | const DummyError = React.createClass({ 16 | render() { 17 | throw new Error(); 18 | } 19 | }); 20 | 21 | describe('Alt Resolver', () => { 22 | let flux; 23 | let altResolver; 24 | 25 | beforeEach(() => { 26 | flux = new Flux(); 27 | altResolver = new AltResolver(); 28 | injectLang(flux); 29 | }); 30 | 31 | it('should map promises on env server', () => { 32 | altResolver.mapPromises().should.be.empty; 33 | altResolver.resolve(function() {}, true); 34 | altResolver.mapPromises().should.not.be.empty; 35 | }); 36 | 37 | it('should render async a dummy component', (done) => { 38 | (async function () { 39 | const content = await altResolver.render(Dummy, flux, true); 40 | should.exist(content); 41 | return done(); 42 | })(); 43 | }); 44 | 45 | it('should not render on browser', (done) => { 46 | (async function () { 47 | const content = await altResolver.render(); 48 | should.not.exist(content); 49 | return done(); 50 | })(); 51 | }); 52 | 53 | it('should render 500 on error', (done) => { 54 | (async function () { 55 | const content = await altResolver.render(DummyError, flux, true); 56 | should.exist(content); 57 | content.body.should.have.string('500'); 58 | return done(); 59 | })(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/spec/utils/image-resolver.test.js: -------------------------------------------------------------------------------- 1 | import imageResolver from 'utils/image-resolver'; 2 | 3 | const should = chai.should(); 4 | 5 | describe('ImageResolver', () => { 6 | // TODO: Set up TDD for server 7 | // `ImageResolver` is mainly used on the server 8 | 9 | it('should throw an error using on browser', () => { 10 | try { 11 | imageResolver(); 12 | } 13 | catch (error) { 14 | should.exist(error); 15 | } 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/spec/utils/intl-loader.test.js: -------------------------------------------------------------------------------- 1 | import intlLoader from 'utils/intl-loader'; 2 | 3 | const should = chai.should(); 4 | 5 | describe('IntlLoader', () => { 6 | it('should load english lang without polyfill', (done) => { 7 | (async function () { 8 | const lang = await intlLoader('en'); 9 | should.exist(lang); 10 | lang.should.be.an('object'); 11 | lang.should.have.any.keys('messages'); 12 | return done(); 13 | })(); 14 | }); 15 | 16 | it('should load french lang without polyfill', (done) => { 17 | (async function () { 18 | const lang = await intlLoader('fr'); 19 | should.exist(lang); 20 | lang.should.be.an('object'); 21 | lang.should.have.any.keys('messages'); 22 | return done(); 23 | })(); 24 | }); 25 | 26 | it('should load english and with polyfill', (done) => { 27 | (async function () { 28 | const lang = await intlLoader('en', true); 29 | should.exist(lang); 30 | lang.should.be.an('object'); 31 | lang.should.have.any.keys('messages'); 32 | return done(); 33 | })(); 34 | }); 35 | 36 | it('should load french and with polyfill', (done) => { 37 | (async function () { 38 | const lang = await intlLoader('fr', true); 39 | should.exist(lang); 40 | lang.should.be.an('object'); 41 | lang.should.have.any.keys('messages'); 42 | return done(); 43 | })(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /test/spec/utils/localized-routes.test.js: -------------------------------------------------------------------------------- 1 | import {generateRoute, replaceParams} from 'utils/localized-routes'; 2 | 3 | chai.should(); 4 | 5 | describe('LocalizedRoutes utils', function() { 6 | it('should return an array of routes', function() { 7 | const routes = generateRoute({ 8 | paths: ['a', 'b', 'c'], 9 | component: function() {} 10 | }); 11 | routes.length.should.eql(3); 12 | }); 13 | 14 | it('should replace params in url', function() { 15 | const route = replaceParams('/foo/:bar/bar/:foo', {bar: 'yolo', foo: 'swag'}); 16 | route.should.eql('/foo/yolo/bar/swag'); 17 | }); 18 | 19 | it('should ignore inexisting params', function() { 20 | const route = replaceParams('/foo', {bar: 'foo'}); 21 | route.should.eql('/foo'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/utils/inject-lang.js: -------------------------------------------------------------------------------- 1 | export default (flux, locale='en') => { 2 | const {messages} = require(`data/${locale}`); 3 | 4 | flux 5 | .getActions('locale') 6 | .switchLocaleSuccess({locale, messages}); 7 | 8 | return flux.getStore('locale').getState(); 9 | }; 10 | -------------------------------------------------------------------------------- /test/utils/stub-router-context.jsx: -------------------------------------------------------------------------------- 1 | import objectAssign from 'react/lib/Object.assign'; 2 | import React, {PropTypes} from 'react'; 3 | 4 | export default function stubRouterContext(Component, props, stubs) { 5 | const RouterStub = objectAssign({ 6 | makePath() {}, 7 | makeHref() {}, 8 | transitionTo() {}, 9 | replaceWith() {}, 10 | goBack() {}, 11 | getCurrentPath() {}, 12 | getCurrentRoutes() {}, 13 | getCurrentPathname() {}, 14 | getCurrentParams() {}, 15 | getCurrentQuery() {}, 16 | isActive() {}, 17 | getRouteAtDepth() {}, 18 | setRouteComponentAtDepth() {} 19 | }, stubs); 20 | 21 | return React.createClass({ 22 | displayName: 'RouterStub', 23 | childContextTypes: { 24 | router: PropTypes.object.isRequired, 25 | routeDepth: PropTypes.number.isRequired 26 | }, 27 | getChildContext() { 28 | return { 29 | router: RouterStub, 30 | routeDepth: 0 31 | }; 32 | }, 33 | render() { 34 | return ; 35 | } 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /tests.webpack.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 'use strict'; 4 | 5 | // Browser ES6 Polyfill 6 | require('babel/polyfill'); 7 | 8 | var context = require.context('./test/spec', true, /\.test\.jsx$|\.test\.js$/); 9 | context.keys().forEach(context); 10 | -------------------------------------------------------------------------------- /webpack/base.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var path = require('path'); 3 | var writeStats = require('./utils/write-stats'); 4 | 5 | var JS_REGEX = /\.js$|\.jsx$|\.es6$|\.babel$/; 6 | 7 | module.exports = { 8 | devtool: 'source-map', 9 | entry: { 10 | app: './app/index.js' 11 | }, 12 | output: { 13 | path: path.resolve(__dirname, '..', 'dist'), 14 | filename: '[name]-[hash].js', 15 | chunkFilename: '[name]-[hash].js', 16 | publicPath: '/assets/' 17 | }, 18 | module: { 19 | preLoaders: [ 20 | { 21 | test: JS_REGEX, 22 | exclude: /node_modules/, 23 | loader: 'eslint' 24 | } 25 | ], 26 | loaders: [ 27 | {test: /\.json$/, exclude: /node_modules/, loader: 'json'}, 28 | {test: JS_REGEX, exclude: /node_modules/, loader: 'babel'} 29 | ], 30 | }, 31 | plugins: [ 32 | // write webpack stats 33 | function() { 34 | this.plugin('done', writeStats); 35 | } 36 | ], 37 | resolve: { 38 | extensions: ['', '.js', '.json', '.jsx', '.es6', '.babel'], 39 | modulesDirectories: ['node_modules', 'app'] 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /webpack/dev-server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | 'use strict'; 4 | 5 | require('babel/register'); 6 | 7 | var debug = require('debug'); 8 | var webpack = require('webpack'); 9 | var WebpackDevServer = require('webpack-dev-server'); 10 | 11 | var config = require('./dev.config'); 12 | 13 | var compiler = webpack(config.webpack); 14 | var devServer = new WebpackDevServer(compiler, config.server.options); 15 | 16 | devServer.listen(config.server.port, '0.0.0.0', function () { 17 | debug('dev')('webpack-dev-server listen on port %s', config.server.port); 18 | }); 19 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import {isArray} from 'lodash'; 3 | 4 | import baseConfig from './base.config'; 5 | import startKoa from './utils/start-koa'; 6 | 7 | const LOCAL_IP = require('dev-ip')(); 8 | 9 | const PROTOCOL = (process.env.C9_HOSTNAME) ? 'https' : 'http'; 10 | const HOST = process.env.C9_HOSTNAME || isArray(LOCAL_IP) && LOCAL_IP[0] || LOCAL_IP || 'localhost'; 11 | const PORT = (process.env.C9_HOSTNAME) ? '443' : parseInt(process.env.PORT, 10) + 1 || 3001; 12 | const PUBLIC_PATH = `${PROTOCOL}://${HOST}:${PORT}/assets/`; 13 | 14 | const config = Object.assign({}, baseConfig, { 15 | devtool: 'eval-source-map', 16 | entry: { 17 | app: [ 18 | `webpack-dev-server/client?http://localhost:${PORT}`, 19 | 'webpack/hot/only-dev-server', 20 | './app/index.js' 21 | ] 22 | }, 23 | output: Object.assign( 24 | baseConfig.output, 25 | {publicPath: PUBLIC_PATH} 26 | ) 27 | }); 28 | 29 | config.module.loaders = config.module.loaders.concat([ 30 | { 31 | test: /\.(jpe?g|png|gif|svg|woff|woff2|eot|ttf)$/, 32 | loader: 'file?name=[sha512:hash:base64:7].[ext]' 33 | }, 34 | { 35 | test: /\.scss|.css$/, 36 | loader: 'style!css?sourceMap!autoprefixer?browsers=last 2 version!sass?outputStyle=expanded&sourceMap' 37 | } 38 | ]); 39 | 40 | // add `react-hot` on JS files 41 | delete config.module.loaders[1].loader; 42 | config.module.loaders[1].loaders = ['react-hot', 'babel']; 43 | 44 | config.plugins = [ 45 | // hot reload 46 | new webpack.HotModuleReplacementPlugin(), 47 | new webpack.NoErrorsPlugin(), 48 | 49 | new webpack.DefinePlugin({ 50 | 'process.env': { 51 | BROWSER: JSON.stringify(true), 52 | NODE_ENV: JSON.stringify('development') 53 | } 54 | }), 55 | 56 | new webpack.optimize.DedupePlugin(), 57 | new webpack.optimize.OccurenceOrderPlugin() 58 | ].concat(config.plugins).concat([ 59 | function() { 60 | this.plugin('done', startKoa); 61 | } 62 | ]); 63 | 64 | export default { 65 | server: { 66 | port: PORT, 67 | options: { 68 | publicPath: (process.env.C9_HOSTNAME) ? '/' : PUBLIC_PATH, 69 | hot: true, 70 | stats: { 71 | assets: true, 72 | colors: true, 73 | version: false, 74 | hash: false, 75 | timings: true, 76 | chunks: false, 77 | chunkModules: false 78 | } 79 | } 80 | }, 81 | webpack: config 82 | }; 83 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('babel/register'); 3 | 4 | var path = require('path'); 5 | var webpack = require('webpack'); 6 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | var baseConfig = require('./base.config'); 8 | 9 | // clean `.tmp` && `dist` 10 | require('./utils/clean-dist')(); 11 | 12 | var config = Object.assign({}, baseConfig); 13 | 14 | config.module.loaders = config.module.loaders.concat([ 15 | { 16 | test: /\.(woff|woff2|eot|ttf)$/, 17 | loader: 'file?name=[sha512:hash:base64:7].[ext]' 18 | }, 19 | { 20 | test: /\.(jpe?g|png|gif|svg)$/, 21 | loader: 'file?name=[sha512:hash:base64:7].[ext]!image?optimizationLevel=7&progressive&interlaced' 22 | }, 23 | { 24 | test: /\.scss|.css$/, 25 | loader: ExtractTextPlugin.extract('style', 'css?sourceMap!autoprefixer?browsers=last 2 version!sass') 26 | } 27 | ]); 28 | 29 | config.plugins = [ 30 | // extract css 31 | new ExtractTextPlugin('[name]-[chunkhash].css'), 32 | 33 | // set env 34 | new webpack.DefinePlugin({ 35 | 'process.env': { 36 | BROWSER: JSON.stringify(true), 37 | NODE_ENV: JSON.stringify('production') 38 | } 39 | }), 40 | 41 | // optimizations 42 | new webpack.optimize.DedupePlugin(), 43 | new webpack.optimize.OccurenceOrderPlugin(), 44 | new webpack.optimize.UglifyJsPlugin({ 45 | compress: { 46 | warnings: false, 47 | screw_ie8: true, 48 | sequences: true, 49 | dead_code: true, 50 | drop_debugger: true, 51 | comparisons: true, 52 | conditionals: true, 53 | evaluate: true, 54 | booleans: true, 55 | loops: true, 56 | unused: true, 57 | hoist_funs: true, 58 | if_return: true, 59 | join_vars: true, 60 | cascade: true, 61 | drop_console: true 62 | }, 63 | output: { 64 | comments: false 65 | } 66 | }) 67 | ].concat(config.plugins); 68 | 69 | module.exports = config; 70 | -------------------------------------------------------------------------------- /webpack/utils/clean-dist.js: -------------------------------------------------------------------------------- 1 | import del from 'del'; 2 | import path from 'path'; 3 | import debug from 'debug'; 4 | 5 | export default () => { 6 | const DIST_PATH = path.resolve(__dirname, '../../dist/*'); 7 | del.sync([DIST_PATH]); 8 | debug('dev')('cleaned `dist` directory'); 9 | }; 10 | -------------------------------------------------------------------------------- /webpack/utils/start-koa.js: -------------------------------------------------------------------------------- 1 | import cp from 'child_process'; 2 | import path from 'path'; 3 | import debug from 'debug'; 4 | import browserSync from 'browser-sync'; 5 | import watch from 'node-watch'; 6 | import {assign, noop} from 'lodash'; 7 | 8 | let server; 9 | let started; 10 | let serverReload; 11 | const KOA_PATH = path.join(__dirname, '../../server/index'); 12 | 13 | const startServer = () => { 14 | // Define `restartServer` 15 | const restartServer = () => { 16 | debug('dev')('restarting koa application'); 17 | serverReload = true; 18 | server.kill('SIGTERM'); 19 | return startServer(); 20 | }; 21 | 22 | // merge env for the new process 23 | const env = assign({}, {NODE_ENV: 'development'}, process.env); 24 | // start the server procress 25 | server = cp.fork(KOA_PATH, {env}); 26 | // when server is `online` 27 | server.once('message', (message) => { 28 | if (message.match(/^online$/)) { 29 | if (serverReload) { 30 | serverReload = false; 31 | browserSync.reload(); 32 | } 33 | if (!started) { 34 | started = true; 35 | 36 | // Start browserSync 37 | browserSync({ 38 | port: parseInt(process.env.PORT, 10) + 2 || 3002, 39 | proxy: `0.0.0.0:${parseInt(process.env.PORT, 10) || 3000}` 40 | }); 41 | 42 | // Listen for `rs` in stdin to restart server 43 | debug('dev')('type `rs` in console for restarting koa application'); 44 | process.stdin.setEncoding('utf8'); 45 | process.stdin.on('data', function(data) { 46 | const parsedData = (data + '').trim().toLowerCase(); 47 | if (parsedData === 'rs') return restartServer(); 48 | }); 49 | 50 | // Start watcher on server files 51 | // and reload browser on change 52 | watch( 53 | path.join(__dirname, '../../server'), 54 | (file) => !file.match('webpack-stats.json') ? restartServer() : noop() 55 | ); 56 | } 57 | } 58 | }); 59 | }; 60 | 61 | // kill server on exit 62 | process.on('exit', () => server.kill('SIGTERM')); 63 | 64 | export default () => !server ? startServer() : noop(); 65 | -------------------------------------------------------------------------------- /webpack/utils/write-stats.js: -------------------------------------------------------------------------------- 1 | // borrowed from https://github.com/gpbl/isomorphic500/blob/master/webpack%2Futils%2Fwrite-stats.js 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import debug from 'debug'; 5 | 6 | const filepath = path.resolve(__dirname, '../../server/webpack-stats.json'); 7 | 8 | export default function(stats) { 9 | const publicPath = this.options.output.publicPath; 10 | const json = stats.toJson(); 11 | 12 | // get chunks by name and extensions 13 | const getChunks = function(name, ext = /.js$/) { 14 | let chunks = json.assetsByChunkName[name]; 15 | 16 | // a chunk could be a string or an array, so make sure it is an array 17 | if (!(Array.isArray(chunks))) { 18 | chunks = [chunks]; 19 | } 20 | 21 | return chunks 22 | .filter(chunk => ext.test(path.extname(chunk))) // filter by extension 23 | .map(chunk => `${publicPath}${chunk}`); // add public path to it 24 | }; 25 | 26 | const script = getChunks('app', /js/); 27 | const style = getChunks('app', /css/); 28 | 29 | // Find compiled images in modules 30 | // it will be used to map original filename to the compiled one 31 | // for server side rendering 32 | const imagesRegex = /\.(jpe?g|png|gif|svg)$/; 33 | const images = json.modules 34 | .filter(module => imagesRegex.test(module.name)) 35 | .map(image => { 36 | return { 37 | original: image.name, 38 | compiled: `${publicPath}${image.assets[0]}` 39 | }; 40 | }); 41 | 42 | const content = {script, style, images}; 43 | 44 | fs.writeFileSync(filepath, JSON.stringify(content)); 45 | debug('dev')('`webpack-stats.json` updated'); 46 | } 47 | --------------------------------------------------------------------------------