├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jscsrc ├── .scss-lint.yaml ├── Procfile ├── README.md ├── app ├── actions │ ├── cars.js │ ├── directors-actions.js │ ├── films-actions.js │ ├── login-actions.js │ └── status-actions.js ├── components │ ├── app.js │ ├── director-profile.js │ ├── directors │ │ ├── director-form.js │ │ ├── directors-table.js │ │ └── directors-table.spec.js │ ├── film-profile.js │ ├── films │ │ ├── film-form.js │ │ ├── films-table.js │ │ └── index.js │ ├── login-new │ │ ├── index.js │ │ └── login-page.js │ ├── login.js │ ├── login.spec.js │ └── shared │ │ ├── action-bar.js │ │ ├── bootstrap-input.js │ │ ├── film-item.js │ │ ├── footer.js │ │ ├── navbar.js │ │ ├── pikaday-input.js │ │ ├── pikaday.js │ │ └── select-input.js ├── data │ ├── en.js │ ├── fr.js │ └── users.json ├── fonts │ ├── Roboto-Medium-webfont.eot │ ├── Roboto-Medium-webfont.svg │ ├── Roboto-Medium-webfont.ttf │ └── Roboto-Medium-webfont.woff ├── images │ ├── favicon.ico │ ├── react-logo.png │ └── spinner.svg ├── index.js ├── main.js ├── pages │ ├── not-found.jsx │ └── server-error.jsx ├── router.js ├── routes.jsx ├── stores │ ├── cars.js │ ├── directors-store.js │ ├── films-store.js │ ├── login-store.js │ ├── login-store.spec.js │ ├── login-store.test.js │ └── status-store.js ├── styles │ ├── _app.scss │ ├── _bootstrap-customizations.scss │ ├── _cars.scss │ ├── _fonts.scss │ ├── _footer.scss │ ├── _main.scss │ ├── _navbar.scss │ ├── _pre-bootstrap-customizations.scss │ ├── _users.scss │ ├── bootstrap-sass.config.js │ └── bootstrap-sass.prod.config.js └── utils │ ├── action-utils.js │ ├── alt-resolver.js │ ├── alt.js │ ├── api.js │ ├── component-utils.js │ ├── countries.js │ ├── flux-old.js │ ├── image-resolver.js │ ├── intl-loader.js │ ├── intl-polyfill.js │ ├── promisify.js │ ├── store-utils.js │ └── test │ ├── get-wrapped-component.js │ ├── init-dom.js │ ├── login-store-mock.js │ ├── router-mock.jsx │ ├── shallow-render-component.js │ └── stub-router-context.js ├── package.json ├── processes.json ├── server ├── config │ ├── all.json │ ├── development.json │ ├── init.js │ └── production.json ├── index.js ├── koa.js ├── models │ ├── car.js │ ├── director.js │ ├── film.js │ └── user.js ├── rest.js ├── restable │ ├── .npmignore │ ├── .travis.yml │ ├── Makefile │ ├── README.md │ ├── docs │ │ ├── coverage.html │ │ ├── docs.md │ │ ├── head.html │ │ ├── tail.html │ │ └── test.html │ ├── example │ │ ├── app.js │ │ └── coffee │ │ │ └── app.coffee │ ├── gulpfile.coffee │ ├── gulpfile.js │ ├── lib │ │ ├── actions.js │ │ ├── index.js │ │ ├── model.js │ │ ├── mount-router.js │ │ └── routes.js │ ├── package.json │ └── test │ │ ├── index.coffee │ │ ├── mocha.opts │ │ └── server.js ├── router.jsx └── views │ ├── layouts │ └── index.hbs │ └── main.hbs ├── test ├── mocha-runner.js └── tape-runner.js ├── tests.webpack.js └── webpack ├── 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 | -------------------------------------------------------------------------------- /.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 = tab 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 | node_modules/** 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": ["react"], 4 | "env": { 5 | "es6": true, 6 | "browser": true, 7 | "node": true, 8 | "mocha": true 9 | }, 10 | "rules": { 11 | "quotes": [0], 12 | "eol-last": [0], 13 | "no-mixed-requires": [0], 14 | "no-underscore-dangle": [0], 15 | "camelcase": 1, 16 | "curly": 0, 17 | "no-unused-expressions": [1], 18 | "react/display-name": 0, 19 | "react/jsx-boolean-value": 1, 20 | "react/jsx-quotes": 1, 21 | "react/jsx-no-undef": 1, 22 | "react/jsx-uses-react": 1, 23 | "react/jsx-uses-vars": 1, 24 | "react/no-did-mount-set-state": 1, 25 | "react/no-did-update-set-state": 1, 26 | "react/no-multi-comp": 1, 27 | "react/no-unknown-property": 1, 28 | "react/prop-types": 1, 29 | "react/react-in-jsx-scope": 1, 30 | "react/self-closing-comp": 1, 31 | "react/wrap-multilines": 1 32 | }, 33 | "ecmaFeatures": { 34 | "jsx": true, 35 | "modules": true, 36 | "blockBindings": true 37 | }, 38 | "globals": { 39 | "sinon": true, 40 | "chai": true, 41 | "assert": true, 42 | "expect": true, 43 | "should": true 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/webpack-stats.json 2 | logs 3 | coverage 4 | node_modules 5 | npm-debug.log 6 | dist 7 | .tmp 8 | .DS_Store 9 | .sass-cache 10 | .env 11 | .c9 -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "airbnb", 3 | "esprima": "./node_modules/esprima-fb", 4 | "requireSemicolons": null, 5 | "validateQuoteMarks": "\"", 6 | "validateIndentation": "\t", 7 | "excludeFiles": ["node_modules/**"] 8 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run prod -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isomorphic React + Flux film database example 2 | 3 | > Complete application example including user authentication using koa on server side, and React/Flux(Alt) on frontend. Also uses koa-mongo-rest to generate REST API, and restful.js to consume them. With a healthy dose of Bootstrap, using react-bootstrap. 4 | 5 | **Demo:** https://react-example-filmdb.herokuapp.com 6 | 7 | 8 | ## Libraries Included 9 | 10 | * [react](https://facebook.github.io/react/) 11 | * [react-router](https://github.com/rackt/react-router) 12 | * [react-hot-loader](https://github.com/gaearon/react-hot-loader) 13 | * [react-bootstrap](http://react-bootstrap.github.io/) 14 | * [alt](https://github.com/goatslacker/alt) 15 | * [iso](https://github.com/goatslacker/iso) 16 | * [koa](http://koajs.com/) 17 | * [webpack](http://webpack.github.io/) 18 | * [babeljs](https://babeljs.io/) 19 | * [koa-mongo-rest](https://github.com/t3chnoboy/koa-mongo-rest) 20 | * [restful.js](https://github.com/marmelab/restful.js) 21 | * [formsy-react](https://github.com/christianalfoni/formsy-react) 22 | 23 | ## TL;DR 24 | 25 | Use with `iojs^1.8.0` or `nodejs^0.12.0`, clone the repo, `npm install` and `npm run dev`. 26 | 27 | Learn React ([react-prime-draft](https://github.com/mikechau/react-primer-draft)), learn Flux and Alt ([alt guide](http://alt.js.org/guide/)). 28 | 29 | Build for production with `npm run build`. 30 | 31 | ## Concepts 32 | 33 | **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. 34 | 35 | With **iso** as helper we can populate **alt** flux stores before the first rendering and have a complete async isomorphic React application. 36 | 37 | **For a more comprehensive guide to the source code, see [wiki](https://github.com/tomaash/react-example-filmdb/wiki)** 38 | 39 | ## Flux 40 | 41 | We use [alt](alt.js.org) singleton as [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html) implementation. 42 | 43 | On the server, it's similar but Flux is initialized using `alt.bootstrap` and for next rendering, the data are emptied using `alt.flush` 44 | 45 | ## Installation / How-to 46 | 47 | I recommend to use [io.js](https://iojs.org/) to take advantages of `ES6` without `--harmony` flag on `NodeJS`. 48 | 49 | It's super easy to do with [nvm](https://github.com/creationix/nvm): 50 | 51 | * `$ nvm install iojs` 52 | * `$ nvm use iojs` 53 | * `$ nvm alias default iojs` (to make `node` default to `iojs`) 54 | 55 | But it works well with `nodejs^0.12.0` as well :) 56 | 57 | ### Run the project in development: 58 | 59 | * `$ npm run dev` 60 | 61 | ### Run tests 62 | 63 | * `$ npm test` will run the tests once 64 | * `$ ./node_modules/.bin/karma start` will watch for changes and run the tests on change 65 | 66 | ### Build project: 67 | 68 | Just run `$ npm run build`, it will produce these tasks: 69 | 70 | * Run tests from `test/spec/**/*.jsx` 71 | * Concat & minify styles to `/dist/app-[hash].css` 72 | * Concat & minify scripts to `/dist/js/app-[hash].js` 73 | 74 | ### Run in production 75 | 76 | Build the project first: 77 | 78 | * `$ npm run build` 79 | 80 | Then start the koa server: 81 | 82 | * `$ NODE_ENV=production node server/index.js` (iojs) 83 | * `$ NODE_ENV=production node --harmony server/index.js` (nodejs 0.12.x) 84 | 85 | 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): 86 | 87 | * `$ pm2 start processes.json` 88 | 89 | -------------------------------------------------------------------------------- /app/actions/cars.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import api from 'utils/api'; 3 | 4 | class CarsActions { 5 | constructor() { 6 | this.generateActions( 7 | 'addSuccess', 'fetchSuccess', 'updateSuccess', 'deleteSuccess' 8 | ); 9 | } 10 | async fetch() { 11 | try { 12 | console.log('will fetch'); 13 | const response = await api.cars.getAll(); 14 | if (response) { 15 | this.actions.fetchSuccess(response().data); 16 | } 17 | } catch (err) { 18 | console.log(err.stack); 19 | } 20 | } 21 | async add(car) { 22 | this.dispatch(car); 23 | const response = await api.cars.post(car); 24 | this.actions.addSuccess(response().data); 25 | } 26 | async update(data, item) { 27 | const response = await api.cars.put(item._id, data); 28 | this.actions.updateSuccess({data: response().data, item: item}); 29 | } 30 | async delete(car, index) { 31 | await api.cars.delete(car._id); 32 | this.actions.deleteSuccess(index); 33 | } 34 | } 35 | 36 | export default CarsActions; 37 | -------------------------------------------------------------------------------- /app/actions/directors-actions.js: -------------------------------------------------------------------------------- 1 | import alt from 'utils/alt'; 2 | import api from 'utils/api'; 3 | import {clone} from 'lodash'; 4 | import {networkAction} from 'utils/action-utils'; 5 | 6 | class DirectorsActions { 7 | constructor() { 8 | this.generateActions('removeCurrent'); 9 | } 10 | fetch() { 11 | networkAction(this, api.directors.getAll); 12 | } 13 | get(id) { 14 | networkAction(this, api.directors.get, id); 15 | } 16 | add(data) { 17 | networkAction(this, api.directors.post, clone(data)); 18 | } 19 | update(id, data) { 20 | networkAction(this, api.directors.put, id, clone(data)); 21 | } 22 | delete(id) { 23 | networkAction(this, api.directors.delete, id); 24 | } 25 | } 26 | 27 | module.exports = (alt.createActions(DirectorsActions)); 28 | -------------------------------------------------------------------------------- /app/actions/films-actions.js: -------------------------------------------------------------------------------- 1 | import alt from 'utils/alt'; 2 | import api from 'utils/api'; 3 | import {clone} from 'lodash'; 4 | import {networkAction} from 'utils/action-utils'; 5 | 6 | class FilmsActions { 7 | fetch() { 8 | networkAction(this, api.films.getAll); 9 | } 10 | get(id) { 11 | networkAction(this, api.films.get, id); 12 | } 13 | findByDirectorId(id) { 14 | networkAction(this, api.films.getAll, { 15 | conditions: JSON.stringify({ 16 | director: id 17 | }) 18 | }); 19 | } 20 | add(data) { 21 | networkAction(this, api.films.post, clone(data)); 22 | } 23 | update(id, data) { 24 | networkAction(this, api.films.put, id, clone(data)); 25 | } 26 | delete(id) { 27 | networkAction(this, api.films.delete, id); 28 | } 29 | } 30 | 31 | module.exports = (alt.createActions(FilmsActions)); 32 | -------------------------------------------------------------------------------- /app/actions/login-actions.js: -------------------------------------------------------------------------------- 1 | import alt from 'utils/alt'; 2 | import axios from 'axios'; 3 | 4 | class LoginActions { 5 | constructor() { 6 | this.generateActions('logout', 'loadLocalUser'); 7 | } 8 | async login(data) { 9 | try { 10 | const response = await axios.post('/auth/login', data); 11 | this.dispatch({ok: true, user: response.data}); 12 | } catch (err) { 13 | console.error(err); 14 | this.dispatch({ok: false, error: err.data}); 15 | } 16 | } 17 | async register(data) { 18 | try { 19 | const response = await axios.post('/auth/register', data); 20 | this.dispatch({ok: true, user: response.data}); 21 | } catch (err) { 22 | console.error(err); 23 | this.dispatch({ok: false, error: err.data}); 24 | } 25 | } 26 | } 27 | 28 | module.exports = (alt.createActions(LoginActions)); 29 | -------------------------------------------------------------------------------- /app/actions/status-actions.js: -------------------------------------------------------------------------------- 1 | import alt from 'utils/alt'; 2 | 3 | class StatusActions { 4 | constructor() { 5 | this.generateActions('started', 'done', 'failed', 'retry'); 6 | } 7 | } 8 | 9 | module.exports = (alt.createActions(StatusActions)); 10 | -------------------------------------------------------------------------------- /app/components/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {RouteHandler} from 'react-router'; 5 | 6 | import Navbar from 'components/shared/navbar'; 7 | import Footer from 'components/shared/footer'; 8 | import Router from 'react-router'; 9 | import reactMixin from 'react-mixin'; 10 | import LoginActions from 'actions/login-actions'; 11 | 12 | @reactMixin.decorate(Router.State) 13 | export default class App extends React.Component { 14 | constructor(props) { 15 | super(props); 16 | LoginActions.loadLocalUser(); 17 | } 18 | render() { 19 | var navbar; 20 | if (this.getPathname() !== '/login') { 21 | navbar = ; 22 | } 23 | return ( 24 |
25 | {navbar} 26 | 27 |
28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/director-profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import connectToStores from 'alt/utils/connectToStores'; 5 | import FilmItem from 'components/shared/film-item'; 6 | 7 | import DirectorsStore from 'stores/directors-store'; 8 | import FilmsStore from 'stores/films-store'; 9 | import StatusStore from 'stores/directors-store'; 10 | import LoginStore from 'stores/login-store'; 11 | import DirectorsActions from 'actions/directors-actions'; 12 | import FilmsActions from 'actions/films-actions'; 13 | import StatusActions from 'actions/status-actions'; 14 | import LoginActions from 'actions/login-actions'; 15 | 16 | @connectToStores 17 | export default class DirectorProfile extends React.Component { 18 | static contextTypes = { 19 | router: React.PropTypes.func 20 | } 21 | // static propTypes = { 22 | // flux: React.PropTypes.object.isRequired 23 | // } 24 | static getStores() { 25 | return [DirectorsStore, FilmsStore, StatusStore]; 26 | } 27 | static getPropsFromStores() { 28 | return { 29 | director: DirectorsStore.getState().currentDirector, 30 | films: FilmsStore.getState().filteredFilms, 31 | status: StatusStore.getState() 32 | }; 33 | } 34 | componentWillMount() { 35 | const id = this.context.router.getCurrentParams().id; 36 | DirectorsActions.get(id); 37 | FilmsActions.findByDirectorId(id); 38 | } 39 | render() { 40 | if (this.props.director === null) { 41 | return ( 42 |

Loading...

43 | ); 44 | } 45 | else if (this.props.director !== '') { 46 | return ( 47 |
48 |

Director profile

49 |
50 |

{this.props.director.name}

51 |

Nationality: {this.props.director.nationality}

52 |

Bio:

53 |

{this.props.director.biography}

54 |

Filmography:

55 | {this.props.films && this.props.films.map((film, index) => 56 | 57 | )} 58 |
59 | ); 60 | } 61 | else { 62 | return ( 63 |

Director not found

64 | ); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/components/directors/director-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Formsy from 'formsy-react'; 3 | import BootstrapInput from 'components/shared/bootstrap-input'; 4 | import PikadayInput from 'components/shared/pikaday-input'; 5 | import SelectInput from 'components/shared/select-input'; 6 | import {Modal, Button} from 'react-bootstrap'; 7 | import countries from 'utils/countries'; 8 | import {defer} from 'lodash'; 9 | 10 | import DirectorsActions from 'actions/directors-actions'; 11 | 12 | export default class DirectorForm extends React.Component { 13 | static propTypes = { 14 | onRequestHide: React.PropTypes.func, 15 | editItem: React.PropTypes.object 16 | } 17 | componentDidMount() { 18 | // Convert birthday to Date object to allow editing 19 | if (this.props.editItem) { 20 | this.props.editItem.birthday = new Date(this.props.editItem.birthday); 21 | } 22 | this.refs.directorForm.reset(this.props.editItem); 23 | } 24 | submit(model) { 25 | if (this.props.editItem) { 26 | DirectorsActions.update(this.props.editItem._id, model); 27 | } 28 | else { 29 | DirectorsActions.add(model); 30 | } 31 | this.refs.directorForm.reset(); 32 | // React complains if we update 33 | // DOM with form validations after close 34 | // so let's wait one tick 35 | defer(this.close.bind(this)); 36 | } 37 | close() { 38 | this.props.onRequestHide(); 39 | } 40 | send() { 41 | this.refs.directorForm.submit(); 42 | } 43 | render() { 44 | var title; 45 | var send; 46 | var nameError = 'Must have at least 2 letters'; 47 | var textError = 'Must have at least 10 letters'; 48 | var nationError = 'Nationality must be selected'; 49 | if (this.props.editItem) { 50 | title = 'Edit director ' + this.props.editItem.name; 51 | send = 'Update'; 52 | } 53 | else { 54 | title = 'Add new director'; 55 | send = 'Create'; 56 | } 57 | return ( 58 | 59 |
60 | 61 | 67 | 73 | 78 | 84 | 85 |
86 |
87 | 88 | 89 |
90 |
91 | ); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/components/directors/directors-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import DirectorForm from './director-form'; 3 | import ActionBar from 'components/shared/action-bar'; 4 | import {ModalTrigger, Button} from 'react-bootstrap'; 5 | import moment from 'moment'; 6 | import connectToStores from 'alt/utils/connectToStores'; 7 | import {authDecorator} from 'utils/component-utils'; 8 | 9 | import DirectorsStore from 'stores/directors-store'; 10 | import DirectorsActions from 'actions/directors-actions'; 11 | 12 | @authDecorator 13 | @connectToStores 14 | export default class DirectorsTable extends React.Component { 15 | static contextTypes = { 16 | router: React.PropTypes.func 17 | } 18 | static propTypes = { 19 | directors: React.PropTypes.array, 20 | directorsHash: React.PropTypes.object 21 | } 22 | static getStores(props) { 23 | return [DirectorsStore]; 24 | } 25 | static getPropsFromStores(props) { 26 | return DirectorsStore.getState(); 27 | } 28 | static willTransitionTo(transition) { 29 | console.log(transition); 30 | } 31 | constructor(props) { 32 | super(props); 33 | this.state = {}; 34 | } 35 | componentWillMount() { 36 | return DirectorsActions.fetch(); 37 | } 38 | add() { 39 | this.refs.modalTrigger.props.modal.props.editItem = null; 40 | this.refs.modalTrigger.show(); 41 | } 42 | render() { 43 | return ( 44 |
45 |

Directors

46 | }> 49 | 50 | 51 | 52 |
53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {this.props.directors && this.props.directors.map((item, index) => 65 | 66 | 67 | 68 | 69 | 70 | 77 | 78 | )} 79 | 80 |
NameNationalityBirthdayBiographyAction
{item.name}{item.nationality}{moment(item.birthday).format('D MMMM YYYY')}{item.biography} 71 | 76 |
81 |
82 | ); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/components/directors/directors-table.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react/addons"; 2 | 3 | import getWrappedComponent from "utils/test/get-wrapped-component"; 4 | import initDom from "utils/test/init-dom"; 5 | 6 | import mockery from "mockery"; 7 | import nock from "nock"; 8 | 9 | import alt from "utils/alt"; 10 | 11 | import DirectorsTable from "components/directors/directors-table"; 12 | 13 | const TestUtils = React.addons.TestUtils; 14 | 15 | describe("Directors table", () => { 16 | 17 | var instance; 18 | var component; 19 | 20 | beforeEach(() => { 21 | mockery.enable({ 22 | warnOnReplace: false, 23 | warnOnUnregistered: false 24 | }); 25 | alt.bootstrap(JSON.stringify({ 26 | DirectorsStore: {directors: [ 27 | {name: 'foo'}, 28 | {name: 'bar'} 29 | ]} 30 | })); 31 | component = React.render(, document.body); 32 | instance = getWrappedComponent(component); 33 | initDom.stashWindow(); 34 | }); 35 | 36 | afterEach(() => { 37 | mockery.disable(); 38 | initDom.restoreWindow(); 39 | nock.enableNetConnect(); 40 | 41 | alt.flush(); 42 | React.unmountComponentAtNode(document.body); 43 | }); 44 | it("should render directors", function() { 45 | const fields = TestUtils.scryRenderedDOMComponentsWithTag(instance, "tr"); 46 | // Table has 3 lines 47 | fields.length.should.eq(3); 48 | // Text content should match 49 | fields[1].props.children[0].props.children.should.eq("foo"); 50 | fields[2].props.children[0].props.children.should.eq("bar"); 51 | }); 52 | it("should render action buttons", function() { 53 | const fields = TestUtils.scryRenderedDOMComponentsWithClass(instance, "action-buttons"); 54 | fields.length.should.eq(2); 55 | }); 56 | 57 | }); 58 | 59 | -------------------------------------------------------------------------------- /app/components/film-profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import connectToStores from 'alt/utils/connectToStores'; 5 | import FilmItem from 'components/shared/film-item'; 6 | 7 | import DirectorsStore from 'stores/directors-store'; 8 | import FilmsStore from 'stores/films-store'; 9 | import DirectorsActions from 'actions/directors-actions'; 10 | import FilmsActions from 'actions/films-actions'; 11 | 12 | @connectToStores 13 | export default class FilmProfile extends React.Component { 14 | static contextTypes = { 15 | router: React.PropTypes.func 16 | } 17 | static getStores(props) { 18 | return [ 19 | FilmsStore, 20 | DirectorsStore 21 | ]; 22 | } 23 | static getPropsFromStores(props) { 24 | const state = { 25 | film: FilmsStore.getState().currentFilm, 26 | director: DirectorsStore.getState().currentDirector 27 | }; 28 | if (state.film && !state.director) { 29 | DirectorsActions.get.defer(state.film.director); 30 | } 31 | return state; 32 | } 33 | componentWillMount() { 34 | const id = this.context.router.getCurrentParams().id; 35 | FilmsActions.get(id); 36 | DirectorsActions.removeCurrent(); 37 | } 38 | render() { 39 | if (this.props.film === null) { 40 | return ( 41 |

Loading...

42 | ); 43 | } 44 | else if (this.props.film !== '') { 45 | return ( 46 |
47 |

Film Profile

48 | 49 |
50 | ); 51 | } 52 | else { 53 | return ( 54 |

Film not found

55 | ); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/components/films/film-form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Formsy from 'formsy-react'; 3 | import BootstrapInput from 'components/shared/bootstrap-input'; 4 | import SelectInput from 'components/shared/select-input'; 5 | import {Modal, Button} from 'react-bootstrap'; 6 | import {defer} from 'lodash'; 7 | 8 | import FilmsActions from 'actions/films-actions'; 9 | 10 | export default class FilmForm extends React.Component { 11 | static propTypes = { 12 | onRequestHide: React.PropTypes.func, 13 | editItem: React.PropTypes.object, 14 | directors: React.PropTypes.array 15 | } 16 | constructor(props) { 17 | super(props); 18 | } 19 | componentDidMount() { 20 | this.refs.filmForm.reset(this.props.editItem); 21 | } 22 | submit(model) { 23 | if (this.props.editItem) { 24 | FilmsActions.update(this.props.editItem._id, model); 25 | } 26 | else { 27 | FilmsActions.add(model); 28 | } 29 | this.refs.filmForm.reset(); 30 | // React complains if we update 31 | // DOM with form validations after close 32 | // so let's wait one tick 33 | defer(this.close.bind(this)); 34 | } 35 | close() { 36 | this.props.onRequestHide(); 37 | } 38 | send() { 39 | this.refs.filmForm.submit(); 40 | } 41 | render() { 42 | var title; 43 | var send; 44 | var nameError = 'Must have at least 2 letters'; 45 | var idError = 'Director must be selected'; 46 | var textError = 'Must have at least 10 letters'; 47 | var yearError = 'Must be a year from 20th or 21st century'; 48 | if (this.props.editItem) { 49 | title = 'Edit film ' + this.props.editItem.name; 50 | send = 'Update'; 51 | } 52 | else { 53 | title = 'Add new film'; 54 | send = 'Create'; 55 | } 56 | return ( 57 | 58 |
59 | 60 | 66 | { 72 | return {value: x._id, label: x.name}; 73 | })}/> 74 | 80 | 86 | 87 |
88 |
89 | 90 | 91 |
92 |
93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/components/films/films-table.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FilmForm from './film-form'; 3 | import {ModalTrigger, Button} from 'react-bootstrap'; 4 | import ActionBar from 'components/shared/action-bar'; 5 | 6 | import DirectorsActions from 'actions/directors-actions'; 7 | import FilmsActions from 'actions/films-actions'; 8 | 9 | export default class FilmsTable extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = {}; 13 | } 14 | static contextTypes = { 15 | router: React.PropTypes.func 16 | } 17 | static propTypes = { 18 | FilmsStore: React.PropTypes.object, 19 | DirectorsStore: React.PropTypes.object 20 | } 21 | componentWillMount() { 22 | FilmsActions.fetch(); 23 | DirectorsActions.fetch(); 24 | } 25 | directorName(id) { 26 | const data = this.props.DirectorsStore.directorsHash[id]; 27 | return data && data.name; 28 | } 29 | add() { 30 | this.refs.modalTrigger.props.modal.props.editItem = null; 31 | this.refs.modalTrigger.show(); 32 | } 33 | render() { 34 | return ( 35 |
36 |

Films

37 | 41 | }> 42 | 43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {this.props.FilmsStore.films && this.props.FilmsStore.films.map((item, index) => 57 | 58 | 59 | 60 | 61 | 62 | 69 | 70 | )} 71 | 72 |
NameDirectorYearDescriptionAction
{item.name}{this.directorName(item.director)}{item.year}{item.description} 63 | 68 |
73 |
74 | ); 75 | } 76 | } 77 | 78 | -------------------------------------------------------------------------------- /app/components/films/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AltContainer from 'alt/AltContainer'; 3 | import FilmsTable from './films-table'; 4 | 5 | import DirectorsStore from 'stores/directors-store'; 6 | import FilmsStore from 'stores/films-store'; 7 | import {authDecorator} from 'utils/component-utils'; 8 | 9 | @authDecorator 10 | export default class Films extends React.Component { 11 | static willTransitionTo(transition) { 12 | console.log(transition); 13 | } 14 | render() { 15 | return ( 16 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/components/login-new/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import AltContainer from 'alt/AltContainer'; 3 | 4 | import LoginStore from 'stores/login-store'; 5 | 6 | import LoginPage from './login-page'; 7 | 8 | export default class Login extends React.Component { 9 | render() { 10 | return ( 11 | 15 | 16 | 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/components/login-new/login-page.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {Input, Button, Alert} from 'react-bootstrap'; 5 | import {changeHandler} from 'utils/component-utils'; 6 | 7 | import LoginActions from 'actions/login-actions'; 8 | // import LoginStore from 'stores/login-store'; 9 | // import LoginActions from 'actions/login-actions'; 10 | 11 | @changeHandler 12 | export default class LoginPage extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | login: {} 17 | }; 18 | } 19 | static contextTypes = { 20 | router: React.PropTypes.func 21 | } 22 | static propTypes = { 23 | error: React.PropTypes.string, 24 | LoginStore: React.PropTypes.object 25 | } 26 | // static getStores() { 27 | // return [LoginStore]; 28 | // } 29 | // static getPropsFromStores() { 30 | // return LoginStore.getState(); 31 | // } 32 | componentWillMount() { 33 | this.state = { 34 | login: {} 35 | }; 36 | } 37 | register() { 38 | LoginActions.register(this.state.login); 39 | } 40 | login() { 41 | LoginActions.login(this.state.login); 42 | } 43 | render() { 44 | var error; 45 | if (this.props.LoginStore.error) { 46 | error = {this.props.LoginStore.error}; 47 | } 48 | return ( 49 |
50 |
51 |

FilmDB

52 |

Watch This™

53 |

Login or create account

54 |
55 | {error} 56 | 61 | 66 | 67 | 68 |
69 |
70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/components/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import {Input, Button, Alert} from 'react-bootstrap'; 5 | import {changeHandler} from 'utils/component-utils'; 6 | 7 | import connectToStores from 'alt/utils/connectToStores'; 8 | 9 | import LoginStore from 'stores/login-store'; 10 | import LoginActions from 'actions/login-actions'; 11 | 12 | @connectToStores 13 | @changeHandler 14 | export default class Login extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | login: {} 19 | }; 20 | } 21 | static contextTypes = { 22 | router: React.PropTypes.func 23 | } 24 | static propTypes = { 25 | error: React.PropTypes.string 26 | } 27 | static getStores() { 28 | return [LoginStore]; 29 | } 30 | static getPropsFromStores() { 31 | return LoginStore.getState(); 32 | } 33 | componentWillMount() { 34 | this.state = { 35 | login: {} 36 | }; 37 | } 38 | register() { 39 | LoginActions.register(this.state.login); 40 | } 41 | login() { 42 | LoginActions.login(this.state.login); 43 | } 44 | render() { 45 | var error; 46 | if (this.props.error) { 47 | error = {this.props.error}; 48 | } 49 | return ( 50 |
51 |
52 |

FilmDB

53 |

Watch This™

54 |

Login or create account

55 |
56 | {error} 57 | 62 | 67 | 68 | 69 |
70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/login.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react/addons"; 2 | 3 | import getWrappedComponent from "utils/test/get-wrapped-component"; 4 | import initDom from "utils/test/init-dom"; 5 | 6 | import mockery from "mockery"; 7 | import nock from "nock"; 8 | import axios from "axios"; 9 | 10 | const TestUtils = React.addons.TestUtils; 11 | 12 | const mockLogin = { 13 | username: "foo", 14 | password: "bar" 15 | }; 16 | 17 | const mockUserResponse = {token: 123}; 18 | 19 | describe("Login", () => { 20 | 21 | var instance; 22 | var component; 23 | 24 | beforeEach(() => { 25 | mockery.enable({ 26 | warnOnReplace: false, 27 | warnOnUnregistered: false 28 | }); 29 | nock.disableNetConnect(); 30 | 31 | const routerMock = require("utils/test/router-mock"); 32 | mockery.registerMock("router", routerMock); 33 | 34 | const Login = require("components/login"); 35 | component = React.render(, document.body); 36 | instance = getWrappedComponent(component); 37 | 38 | initDom.stashWindow(); 39 | }); 40 | 41 | afterEach(() => { 42 | mockery.disable(); 43 | initDom.restoreWindow(); 44 | nock.enableNetConnect(); 45 | React.unmountComponentAtNode(document.body); 46 | }); 47 | it("should have input fields", function() { 48 | const fields = TestUtils.scryRenderedDOMComponentsWithTag(instance, "input"); 49 | fields.length.should.eq(2); 50 | }); 51 | 52 | }); 53 | 54 | -------------------------------------------------------------------------------- /app/components/shared/action-bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from 'react-bootstrap'; 3 | 4 | export default class ActionBar extends React.Component { 5 | static contextTypes = { 6 | router: React.PropTypes.func 7 | } 8 | static propTypes = { 9 | item: React.PropTypes.object, 10 | modalTrigger: React.PropTypes.object, 11 | deleteAction: React.PropTypes.func, 12 | showRoute: React.PropTypes.string 13 | } 14 | delete() { 15 | this.props.deleteAction(this.props.item._id); 16 | } 17 | edit() { 18 | this.props.modalTrigger.props.modal.props.editItem = this.props.item; 19 | this.props.modalTrigger.show(); 20 | } 21 | showProfile() { 22 | return this.context.router.transitionTo(this.props.showRoute, {id: this.props.item._id}); 23 | } 24 | render() { 25 | return ( 26 |
27 | 28 | 32 | 36 | 40 | 41 |
42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/components/shared/bootstrap-input.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import React from 'react'; 4 | import Formsy from 'formsy-react'; 5 | 6 | export default React.createClass({ 7 | displayName: 'BootstrapInput', 8 | // Add the Formsy Mixin 9 | mixins: [Formsy.Mixin], 10 | propTypes: { 11 | name: React.PropTypes.string.isRequired, 12 | title: React.PropTypes.string.isRequired, 13 | type: React.PropTypes.string.isRequired 14 | }, 15 | 16 | // setValue() will set the value of the component, which in 17 | // turn will validate it and the rest of the form 18 | changeValue: function (event) { 19 | this.setValue(event.currentTarget.value); 20 | }, 21 | render: function () { 22 | 23 | // Set a specific className based on the validation 24 | // state of this component. showRequired() is true 25 | // when the value is empty and the required prop is 26 | // passed to the input. showError() is true when the 27 | // value typed is invalid 28 | var className = this.showRequired() ? 'required' : (!this.isPristine() && this.showError()) ? 'error' : ''; 29 | 30 | // An error message is returned ONLY if the component is invalid 31 | // or the server has returned an error message 32 | var errorMessage = this.getErrorMessage(); 33 | 34 | var inputClass = this.props.type === 'textarea' ? 'textarea' : 'input'; 35 | 36 | return ( 37 |
38 | 39 | {React.createElement(inputClass, { 40 | className: 'form-control', 41 | type: this.props.type || 'text', 42 | name: this.props.name, 43 | onChange: this.changeValue, 44 | value: this.getValue() 45 | })} 46 | {errorMessage} 47 |
48 | ); 49 | } 50 | }); 51 | -------------------------------------------------------------------------------- /app/components/shared/film-item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class FilmItem extends React.Component { 4 | static propTypes = { 5 | film: React.PropTypes.object, 6 | director: React.PropTypes.object 7 | } 8 | render() { 9 | var director; 10 | if (this.props.director) { 11 | director =

Director: {this.props.director.name}

; 12 | } 13 | const film = this.props.film; 14 | return ( 15 |
16 |
17 |

{film && film.name}

18 | {director} 19 |

20 | Year: {film && film.year} 21 |

22 |

Storyline:

23 |

{film && film.description}

24 |
25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/components/shared/footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import connectToStores from 'alt/utils/connectToStores'; 3 | import StatusStore from 'stores/status-store'; 4 | 5 | @connectToStores 6 | export default class Footer extends React.Component { 7 | static getStores() { 8 | return [ 9 | StatusStore 10 | ]; 11 | } 12 | static getPropsFromStores() { 13 | return StatusStore.getState(); 14 | } 15 | render() { 16 | return ( 17 |
18 |
19 |
20 |

Created by Tomas Holas for educational purposes. Please do not abuse.

21 |
22 |
23 | ); 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /app/components/shared/navbar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import connectToStores from 'alt/utils/connectToStores'; 3 | import {Link} from 'react-router'; 4 | import {NavItemLink} from 'react-router-bootstrap'; 5 | import {Alert, Button} from 'react-bootstrap'; 6 | 7 | import StatusStore from 'stores/status-store'; 8 | import StatusActions from 'actions/status-actions'; 9 | import LoginActions from 'actions/login-actions'; 10 | 11 | @connectToStores 12 | export default class Navbar extends React.Component { 13 | static contextTypes = { 14 | router: React.PropTypes.func 15 | } 16 | static getStores() { 17 | return [ 18 | StatusStore 19 | ]; 20 | } 21 | static getPropsFromStores() { 22 | return StatusStore.getState(); 23 | } 24 | retry() { 25 | StatusActions.retry(); 26 | } 27 | logout() { 28 | LoginActions.logout(); 29 | } 30 | render() { 31 | var errorComponent; 32 | var retryComponent; 33 | var busyComponent; 34 | if (this.props.error) { 35 | if (this.props.retryData) { 36 | retryComponent = ; 37 | } 38 | errorComponent = ( 39 | 40 | Network Error! 41 | {retryComponent} 42 | ); 43 | } 44 | // Prerender busy on server as not to lose markup state on client 45 | if (this.props.busy || !process.env.BROWSER) { 46 | busyComponent =
; 47 | } 48 | return ( 49 |
50 | 73 | {errorComponent} 74 |
75 | ); 76 | } 77 | } 78 | 79 | 80 | -------------------------------------------------------------------------------- /app/components/shared/pikaday-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Formsy from 'formsy-react'; 3 | import Pikaday from 'components/shared/pikaday'; 4 | 5 | export default React.createClass({ 6 | displayName: 'PikadayInput', 7 | // Add the Formsy Mixin 8 | mixins: [Formsy.Mixin], 9 | propTypes: { 10 | name: React.PropTypes.string.isRequired, 11 | title: React.PropTypes.string.isRequired, 12 | type: React.PropTypes.string.isRequired 13 | }, 14 | 15 | // setValue() will set the value of the component, which in 16 | // turn will validate it and the rest of the form 17 | changeValue: function (event) { 18 | // console.log(event.toDateString()); 19 | this.setValue(event); 20 | }, 21 | render: function () { 22 | 23 | // Set a specific className based on the validation 24 | // state of this component. showRequired() is true 25 | // when the value is empty and the required prop is 26 | // passed to the input. showError() is true when the 27 | // value typed is invalid 28 | var className = this.showRequired() ? 'required' : (!this.isPristine() && this.showError()) ? 'error' : ''; 29 | 30 | // An error message is returned ONLY if the component is invalid 31 | // or the server has returned an error message 32 | var errorMessage = this.getErrorMessage(); 33 | 34 | return ( 35 |
36 | 37 | 43 | {errorMessage} 44 |
45 | ); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /app/components/shared/pikaday.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var Pikaday; 3 | 4 | // This will be used by Pikaday 5 | /*eslint no-unused-vars:0*/ 6 | var moment; 7 | 8 | if (process.env.BROWSER) { 9 | moment = require('moment'); 10 | Pikaday = require('pikaday'); 11 | require('pikaday/css/pikaday.css'); 12 | } 13 | 14 | var ReactPikaday = React.createClass({ 15 | propTypes: { 16 | className: React.PropTypes.string, 17 | placeholder: React.PropTypes.string, 18 | format: React.PropTypes.string, 19 | value: React.PropTypes.instanceOf(Date), 20 | onChange: React.PropTypes.func, 21 | 22 | valueLink: React.PropTypes.shape({ 23 | value: React.PropTypes.instanceOf(Date), 24 | requestChange: React.PropTypes.func.isRequired 25 | }) 26 | }, 27 | 28 | getValueLink: function(props) { 29 | return props.valueLink || { 30 | value: props.value, 31 | requestChange: props.onChange 32 | }; 33 | }, 34 | 35 | setDateIfChanged: function(newDate, prevDate) { 36 | var newTime = newDate ? newDate.getTime() : null; 37 | var prevTime = prevDate ? prevDate.getTime() : null; 38 | 39 | if (newTime !== prevTime) { 40 | if (newDate === null) { 41 | // Workaround for pikaday not clearing value when date set to falsey 42 | this.refs.pikaday.getDOMNode().value = ''; 43 | } 44 | this._picker.setDate(newDate, true); // 2nd param = don't call onSelect 45 | } 46 | }, 47 | 48 | componentDidMount: function() { 49 | var el = this.refs.pikaday.getDOMNode(); 50 | this._picker = new Pikaday({ 51 | field: el, 52 | format: this.props.format, 53 | onSelect: this.getValueLink(this.props).requestChange 54 | }); 55 | 56 | this.setDateIfChanged(this.getValueLink(this.props).value); 57 | }, 58 | 59 | componentWillReceiveProps: function(nextProps) { 60 | var newDate = this.getValueLink(nextProps).value; 61 | var lastDate = this.getValueLink(this.props).value; 62 | 63 | this.setDateIfChanged(newDate, lastDate); 64 | }, 65 | 66 | render: function() { 67 | return ( 68 | 70 | ); 71 | } 72 | }); 73 | 74 | module.exports = ReactPikaday; 75 | -------------------------------------------------------------------------------- /app/components/shared/select-input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Formsy from 'formsy-react'; 3 | import Select from 'react-select'; 4 | 5 | if (process.env.BROWSER) { 6 | require('react-select/dist/default.css'); 7 | } 8 | 9 | export default React.createClass({ 10 | displayName: 'SelectInput', 11 | // Add the Formsy Mixin 12 | mixins: [Formsy.Mixin], 13 | propTypes: { 14 | name: React.PropTypes.string.isRequired, 15 | title: React.PropTypes.string.isRequired, 16 | options: React.PropTypes.array.isRequired 17 | }, 18 | 19 | // setValue() will set the value of the component, which in 20 | // turn will validate it and the rest of the form 21 | changeValue: function (event) { 22 | // console.log(event.toDateString()); 23 | this.setValue(event); 24 | }, 25 | render: function () { 26 | 27 | // Set a specific className based on the validation 28 | // state of this component. showRequired() is true 29 | // when the value is empty and the required prop is 30 | // passed to the input. showError() is true when the 31 | // value typed is invalid 32 | var className = this.showRequired() ? 'required' : (!this.isPristine() && this.showError()) ? 'error' : ''; 33 | 34 | // An error message is returned ONLY if the component is invalid 35 | // or the server has returned an error message 36 | var errorMessage = this.getErrorMessage(); 37 | 38 | return ( 39 |
40 | 41 |