├── src ├── pages │ ├── AboutPage │ │ ├── AboutPage.scss │ │ ├── AboutPage.spec.js │ │ ├── AboutPage.i18n.js │ │ └── AboutPage.js │ ├── ProfileEditPage │ │ ├── ProfileEditPage.i18n.js │ │ └── ProfileEditPage.js │ └── LandingPage │ │ ├── LandingPage.scss │ │ ├── LandingPage.i18n.js │ │ ├── LandingPageHero.js │ │ └── LandingPage.js ├── containers │ ├── MainHeader │ │ ├── MainHeader.scss │ │ └── MainHeader.js │ ├── DevTools.js │ ├── ProfileEditForm │ │ ├── ProfileEditForm.scss │ │ ├── ProfileEditForm.validations.js │ │ ├── ProfileEditForm.i18n.js │ │ └── ProfileEditForm.js │ ├── HeroPageLayout.js │ ├── AdminPageLayout.js │ ├── Root.js │ ├── AppContainer.js │ ├── Sidebar │ │ └── Sidebar.js │ ├── AuthenticatedComponent.js │ └── LanguageSelectionDropdown │ │ └── LanguageSelectionDropdown.js ├── static │ ├── robots.txt │ ├── humans.txt │ ├── favicon.png │ ├── images │ │ ├── facebook_logo.png │ │ ├── twitter_logo.png │ │ ├── workspace-ca.jpg │ │ ├── workspace-cb.jpg │ │ ├── workspace-cc.jpg │ │ ├── default-profile.png │ │ ├── twitter_logo_2x.png │ │ └── facebook_logo_2x.png │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── api │ │ └── profile.json ├── declarations │ ├── i18n-types.js │ └── app.js ├── translations │ ├── index.js │ ├── es.js │ └── en.js ├── app-config.js ├── components │ ├── Hero │ │ ├── index.js │ │ ├── HeroBackground.js │ │ ├── HeroContent.js │ │ ├── Hero.js │ │ └── Hero.scss │ ├── Login │ │ └── Login.js │ ├── VAlign │ │ ├── VAlign.scss │ │ └── VAlign.js │ ├── DropdownProfileCard │ │ ├── DropdownProfileCard.scss │ │ ├── DropdownProfileCard.js │ │ └── DropdownProfileCard.spec.js │ ├── Spinner │ │ ├── Spinner.scss │ │ └── Spinner.js │ ├── FormFields │ │ ├── DropDown.js │ │ ├── TextInput.js │ │ ├── HorizontalRadioGroup.js │ │ ├── DropDown.spec.js │ │ ├── FormErrorMessages.js │ │ ├── TextInput.spec.js │ │ └── HorizontalRadioGroup.spec.js │ ├── UserDropdownMenu │ │ └── UserDropdownMenu.js │ ├── MainFooter │ │ ├── MainFooter.scss │ │ └── MainFooter.js │ └── DocumentTitle.js ├── styles │ ├── app.scss │ ├── base.scss │ ├── admin.scss │ └── vendor │ │ ├── normalize.css │ │ └── skeleton.css ├── redux │ ├── root-reducer.js │ ├── modules │ │ ├── language │ │ │ └── language.js │ │ ├── auth │ │ │ ├── auth-reducer.js │ │ │ └── auth-actions.js │ │ ├── user │ │ │ ├── user-reducer.js │ │ │ ├── user-actions.spec.js │ │ │ └── user-actions.js │ │ ├── document-title │ │ │ └── document-title.js │ │ └── spinner │ │ │ └── spinner.js │ └── configure-store.js ├── app.js ├── shared │ ├── forms.js │ └── links.js ├── api │ └── user.js ├── index.html └── routes.js ├── interfaces └── debug.js ├── .gitignore ├── .eslintignore ├── tests.karma.js ├── .editorconfig ├── CHANGELOG.md ├── config ├── environments │ ├── development.js │ └── production.js └── index.js ├── .eslintrc ├── .travis.yml ├── .babelrc ├── .flowconfig ├── LICENSE ├── FLOWTYPE.md ├── karma.conf.js ├── server └── index.js ├── CONTRIBUTING.md ├── EDITORS.md ├── CODE_OF_CONDUCT.md ├── package.json ├── webpack.config.js └── README.md /src/pages/AboutPage/AboutPage.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/AboutPage/AboutPage.spec.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/containers/MainHeader/MainHeader.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /interfaces/debug.js: -------------------------------------------------------------------------------- 1 | declare var __DEBUG__: boolean; 2 | -------------------------------------------------------------------------------- /src/static/humans.txt: -------------------------------------------------------------------------------- 1 | # Check it out: http://humanstxt.org/ 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.iml 3 | dist/ 4 | node_modules/ 5 | coverage/ 6 | .DS_Store 7 | *.log 8 | *.map 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage/** 2 | node_modules/** 3 | dist/** 4 | *.spec.js 5 | src/index.html 6 | interfaces/** 7 | -------------------------------------------------------------------------------- /src/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/favicon.png -------------------------------------------------------------------------------- /src/static/images/facebook_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/facebook_logo.png -------------------------------------------------------------------------------- /src/static/images/twitter_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/twitter_logo.png -------------------------------------------------------------------------------- /src/static/images/workspace-ca.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/workspace-ca.jpg -------------------------------------------------------------------------------- /src/static/images/workspace-cb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/workspace-cb.jpg -------------------------------------------------------------------------------- /src/static/images/workspace-cc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/workspace-cc.jpg -------------------------------------------------------------------------------- /src/static/images/default-profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/default-profile.png -------------------------------------------------------------------------------- /src/static/images/twitter_logo_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/twitter_logo_2x.png -------------------------------------------------------------------------------- /src/static/images/facebook_logo_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/images/facebook_logo_2x.png -------------------------------------------------------------------------------- /tests.karma.js: -------------------------------------------------------------------------------- 1 | // require all `tests/**/*.spec.js` 2 | const context = require.context('./src', true, /\.spec\.js$/); 3 | context.keys().forEach(context); 4 | -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/declarations/i18n-types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type FormattedMessageType = { 4 | id: string; 5 | defaultMessage: string; 6 | description?: string; 7 | }; 8 | -------------------------------------------------------------------------------- /src/static/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencredo/opencredo-react-boilerplate/HEAD/src/static/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/translations/index.js: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import es from './es'; 3 | 4 | const messages = { 5 | en, 6 | es, 7 | }; 8 | 9 | export default messages; 10 | -------------------------------------------------------------------------------- /src/pages/ProfileEditPage/ProfileEditPage.i18n.js: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | title: { 3 | id: 'profile.edit.title', 4 | defaultMessage: 'Edit Profile', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/app-config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'OpenCredo React Boilerplate', 3 | description: 'Universal web application boilerplate built with React, Redux and React-Router.', 4 | }; 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | end_of_line = lf 4 | indent_size = 2 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | 8 | [*.md] 9 | max_line_length = 0 10 | trim_trailing_whitespace = false 11 | -------------------------------------------------------------------------------- /src/pages/LandingPage/LandingPage.scss: -------------------------------------------------------------------------------- 1 | @import "base"; 2 | 3 | .title { 4 | font-weight: bold; 5 | } 6 | 7 | 8 | .hero-title { 9 | font-weight: bold; 10 | color: $white; 11 | text-transform: uppercase; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/Hero/index.js: -------------------------------------------------------------------------------- 1 | import Hero from './Hero'; 2 | import HeroBackground from './HeroBackground'; 3 | import HeroContent from './HeroContent'; 4 | 5 | export { 6 | Hero, 7 | HeroContent, 8 | HeroBackground, 9 | }; 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## 1.0.2 (2016-03-02) 5 | * Updated package dependencies 6 | 7 | ## 1.0.1 (2015-02-04) 8 | 9 | * Initial public release (and first Changelog entry) 10 | -------------------------------------------------------------------------------- /src/pages/AboutPage/AboutPage.i18n.js: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | title: { 3 | id: 'aboutPage.title', 4 | defaultMessage: 'About Us', 5 | }, 6 | overview: { 7 | id: 'aboutPage.overview', 8 | defaultMessage: 'overview', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/components/Login/Login.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const Login = (props) => 4 | Login; 5 | 6 | 7 | Login.propTypes = { 8 | onClick: PropTypes.func.isRequired, 9 | }; 10 | 11 | export default Login; 12 | -------------------------------------------------------------------------------- /config/environments/development.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: { 3 | devtool: 'cheap-module-eval-source-map', 4 | }, 5 | 6 | compiler: { 7 | hash_type: 'hash', 8 | stats: { 9 | chunks: false, 10 | chunkModules: false, 11 | colors: true, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/declarations/app.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | // store custom type declarations here 4 | 5 | export type User = { 6 | userId: string; 7 | name: string; 8 | givenName: string; 9 | familyName: string; 10 | nickname: string; 11 | picture: string; 12 | email: string; 13 | emailVerified: boolean; 14 | roles: string[]; 15 | createdAt: string; 16 | updatedAt: string; 17 | locale: string; 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/VAlign/VAlign.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | display: table; 3 | position: relative; 4 | } 5 | 6 | .horizontal { 7 | width: 100%; 8 | } 9 | 10 | .vertical { 11 | height: 100%; 12 | } 13 | .top, .middle, .bottom { 14 | display: table-cell; 15 | } 16 | 17 | .top { 18 | vertical-align: top; 19 | } 20 | 21 | .middle { 22 | vertical-align: middle; 23 | } 24 | 25 | .bottom { 26 | vertical-align: bottom; 27 | } 28 | -------------------------------------------------------------------------------- /config/environments/production.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | server: { 3 | hostname: '', 4 | port: 80, 5 | }, 6 | 7 | webpack: { 8 | devtool: 'source-map', 9 | output: { 10 | publicPath: '/', 11 | }, 12 | }, 13 | 14 | compiler: { 15 | hash_type: 'chunkhash', 16 | stats: { 17 | chunks: true, 18 | chunkModules: true, 19 | colors: true, 20 | }, 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /src/components/Hero/HeroBackground.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Hero.scss'; 3 | 4 | export default class HeroBackground extends React.Component { 5 | static propTypes = { 6 | image: React.PropTypes.string.isRequired, 7 | }; 8 | 9 | render() { 10 | return ( 11 |
12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Hero/HeroContent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Hero.scss'; 3 | 4 | export default class HeroContent extends React.Component { 5 | static propTypes = { 6 | children: React.PropTypes.node, 7 | }; 8 | 9 | render() { 10 | const children = this.props.children; 11 | return ( 12 |
13 | {children || ''} 14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/styles/app.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | // Some best-practice CSS that's useful for most apps 4 | // Just remove them if they're not what you want 5 | html { 6 | box-sizing: border-box; 7 | } 8 | 9 | html, body { 10 | margin: 0; 11 | padding: 0; 12 | height: 100%; 13 | } 14 | 15 | *, 16 | *:before, 17 | *:after { 18 | box-sizing: inherit; 19 | } 20 | body { 21 | font-family: $fonts; 22 | } 23 | 24 | a { 25 | cursor: pointer; 26 | } 27 | -------------------------------------------------------------------------------- /src/styles/base.scss: -------------------------------------------------------------------------------- 1 | $padding-size: 25px; 2 | 3 | $color-footer: #E9E9E9; 4 | 5 | $fonts: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif; 6 | 7 | 8 | // colors 9 | $white: #f5f5f5; 10 | $light-grey: #f5f5f5; 11 | $medium-grey: #82888a; 12 | $dark-grey: #565a5c; 13 | 14 | 15 | // admin 16 | $admin-nav-background-color: #363c4a; 17 | $admin-nav-text-color: #bbb; 18 | $admin-nav-dropdown-menu-bg: #ffffff; 19 | $admin-nav-dropdown-menu-color: #777888; 20 | -------------------------------------------------------------------------------- /src/containers/ProfileEditForm/ProfileEditForm.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 13px 0; 3 | } 4 | 5 | .picture { 6 | display: inline-block; 7 | max-width: 100%; 8 | max-height: 100%; 9 | border-radius: 10px; 10 | } 11 | 12 | .details { 13 | display: inline-block; 14 | padding: 0 0 0 14px; 15 | vertical-align: top; 16 | } 17 | 18 | .fullName { 19 | font-size: 24px; 20 | font-weight: 400; 21 | } 22 | 23 | .updateButton { 24 | margin-top: 20px; 25 | } 26 | -------------------------------------------------------------------------------- /src/styles/admin.scss: -------------------------------------------------------------------------------- 1 | @import 'base'; 2 | 3 | #content { 4 | margin-top: 1rem; 5 | } 6 | 7 | .box { 8 | background-color: white; 9 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.02); 10 | border: 1px solid #DBDBDC; 11 | border-radius: 3px; 12 | min-height: 20px; 13 | } 14 | 15 | .box-title { 16 | margin: 0; 17 | border-bottom: 1px solid black; 18 | font-size: 15px; 19 | padding: 0 15px; 20 | color: #4b4f5d; 21 | border-bottom-color: #f2f2f2; 22 | font-size: 15px; 23 | line-height: 48px; 24 | } 25 | -------------------------------------------------------------------------------- /src/static/api/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "userId": "1", 3 | "name": "John Doe", 4 | "givenName": "John", 5 | "familyName": "Doe", 6 | "nickname": "john.doe", 7 | "picture": "/images/default-profile.png", 8 | "email": "john.doe@example.org", 9 | "emailVerified": true, 10 | "roles": [ 11 | "admin" 12 | ], 13 | "gender": "male", 14 | "age": 46, 15 | "notes": "this\nand that", 16 | "createdAt": "2016-01-01T00:00:00.000Z", 17 | "updatedAt": "2016-01-01T00:00:00.000Z", 18 | "locale": "en-GB" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/DropdownProfileCard/DropdownProfileCard.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | position: relative; 3 | min-height: 40px; 4 | padding: 13px 13px 13px 56px !important; 5 | margin: 0 !important; 6 | } 7 | 8 | .picture { 9 | display: block; 10 | position: absolute !important; 11 | left: 14px; 12 | top: 14px; 13 | width: 31px; 14 | height: 31px; 15 | border-radius: 20px; 16 | } 17 | 18 | .nickname { 19 | display: block; 20 | } 21 | 22 | .username { 23 | display: block; 24 | font-weight: 800; 25 | color: #454545; 26 | } 27 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "mocha": true, 9 | }, 10 | "globals": { 11 | "DEVELOPMENT": false, 12 | "PRODUCTION": false, 13 | "__DEBUG__": false, 14 | "sinon": false, 15 | "expect": false, 16 | "assert": false 17 | }, 18 | "rules": { 19 | "no-console": 2, 20 | "block-spacing": [2, "always"], 21 | "new-parens": 2, 22 | "no-dupe-class-members": 2, 23 | "no-this-before-super": 1, 24 | "prefer-template": 2, 25 | "max-len": [1, 120] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | -webkit-animation: spin 1000ms infinite linear; 3 | animation: spin 1000ms infinite linear; 4 | margin-right: 10px; 5 | } 6 | 7 | @-webkit-keyframes spin { 8 | 0% { 9 | -webkit-transform: rotate(0deg); 10 | transform: rotate(0deg); 11 | } 12 | 100% { 13 | -webkit-transform: rotate(359deg); 14 | transform: rotate(359deg); 15 | } 16 | } 17 | 18 | @keyframes spin { 19 | 0% { 20 | -webkit-transform: rotate(0deg); 21 | transform: rotate(0deg); 22 | } 23 | 100% { 24 | -webkit-transform: rotate(359deg); 25 | transform: rotate(359deg); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/redux/root-reducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routeReducer as routing } from 'react-router-redux'; 3 | import { documentTitleReducer as documentTitle } from './modules/document-title/document-title'; 4 | import { spinnerReducer as spinner } from './modules/spinner/spinner'; 5 | import { languageReducer as language } from './modules/language/language'; 6 | import { reducer as form } from 'redux-form'; 7 | import user from './modules/user/user-reducer'; 8 | import auth from './modules/auth/auth-reducer'; 9 | 10 | export default combineReducers({ 11 | auth, 12 | spinner, 13 | user, 14 | language, 15 | routing, 16 | documentTitle, 17 | form, 18 | }); 19 | -------------------------------------------------------------------------------- /src/components/FormFields/DropDown.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Element } from 'react'; 3 | import { Input } from 'react-bootstrap'; 4 | 5 | class DropDown extends React.Component { 6 | static propTypes = { 7 | label: PropTypes.string.isRequired, 8 | field: PropTypes.object.isRequired, 9 | values: PropTypes.array.isRequired, 10 | }; 11 | 12 | render(): Element { 13 | const { label, field, values } = this.props; 14 | 15 | return ( 16 | 17 | {values.map(value => )} 18 | 19 | ); 20 | } 21 | } 22 | 23 | export default DropDown; 24 | -------------------------------------------------------------------------------- /src/components/Hero/Hero.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './Hero.scss'; 3 | import classNames from 'classnames'; 4 | 5 | export default class Hero extends React.Component { 6 | static propTypes = { 7 | children: React.PropTypes.node, 8 | displayUnderNavbar: React.PropTypes.bool, 9 | small: React.PropTypes.bool, 10 | }; 11 | 12 | render() { 13 | const classes = classNames({ 14 | [styles.hero]: true, 15 | [styles['move-up']]: this.props.displayUnderNavbar, 16 | [styles['hero-small']]: this.props.small, 17 | }); 18 | 19 | return ( 20 |
21 | {this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Hero/Hero.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | height: 600px; 3 | position: relative; 4 | background: black; 5 | color: white; 6 | transition: height .3s; 7 | } 8 | 9 | .move-up { 10 | margin-top: -50px; 11 | } 12 | 13 | .content { 14 | height: 550px; 15 | top: 50px; 16 | position: relative; 17 | padding-bottom: 100px; 18 | padding-left: 25px; 19 | padding-right: 25px; 20 | } 21 | 22 | .background { 23 | position: absolute; 24 | width: 100%; 25 | height: 100%; 26 | background-size: cover; 27 | overflow: hidden; 28 | img { 29 | width: 100%; 30 | height: auto; 31 | } 32 | } 33 | 34 | .hero-small { 35 | height: 400px; 36 | 37 | .content { 38 | height: 350px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/DropdownProfileCard/DropdownProfileCard.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Link } from 'react-router'; 3 | import styles from './DropdownProfileCard.scss'; 4 | 5 | const DropdownProfileCard = ({ picture, name, nickname }) => 6 | ( 7 | 8 | 9 | {name} 10 | {nickname} 11 | 12 | ); 13 | 14 | 15 | DropdownProfileCard.propTypes = { 16 | picture: PropTypes.string.isRequired, 17 | name: PropTypes.string.isRequired, 18 | nickname: PropTypes.string, 19 | }; 20 | 21 | export default DropdownProfileCard; 22 | -------------------------------------------------------------------------------- /src/components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Modal } from 'react-bootstrap'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import styles from './Spinner.scss'; 5 | 6 | const Spinner = ({ canShow, messageId }) => { 7 | const className = `glyphicon glyphicon-refresh ${styles.spinner}`; 8 | 9 | return ( 10 | 11 | 12 | 13 | {canShow && 14 | 15 | } 16 | 17 | 18 | ); 19 | }; 20 | 21 | Spinner.propTypes = { 22 | canShow: PropTypes.bool.isRequired, 23 | messageId: PropTypes.string, 24 | }; 25 | 26 | export default Spinner; 27 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { browserHistory } from 'react-router'; 4 | import configureStore from './redux/configure-store'; 5 | import routes from './routes'; 6 | import Root from './containers/Root'; 7 | import debug from 'debug'; 8 | import { addLocaleData } from 'react-intl'; 9 | import en from 'react-intl/locale-data/en'; 10 | import es from 'react-intl/locale-data/es'; 11 | 12 | addLocaleData(en); 13 | addLocaleData(es); 14 | 15 | if (__DEBUG__) { 16 | debug.enable('app:*'); 17 | } 18 | 19 | const store = configureStore({}, browserHistory); 20 | 21 | // Render the React application to the DOM 22 | ReactDOM.render( 23 | , 24 | document.getElementById('root') 25 | ); 26 | -------------------------------------------------------------------------------- /src/pages/LandingPage/LandingPage.i18n.js: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | title: { 3 | id: 'landingPage.title', 4 | defaultMessage: 'Landing Page', 5 | }, 6 | button: { 7 | clickMe: { 8 | id: 'landingPage.button.clickMe', 9 | defaultMessage: 'Landing Page', 10 | }, 11 | }, 12 | para: { 13 | pressCtrlH: { 14 | id: 'landingPage.para.pressCtrlH', 15 | defaultMessage: 'Press Ctrl-H for Redux Dev Tools.', 16 | }, 17 | autoUpdate: { 18 | id: 'landingPage.para.autoUpdate', 19 | defaultMessage: 'This page auto-updates.', 20 | }, 21 | es7Decorator: { 22 | id: 'landingPage.para.es7Decorator', 23 | defaultMessage: 'It also demonstrates use of the @autobind ES7 decorator.', 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /src/shared/forms.js: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | error: { 3 | email: { 4 | id: 'common.form.error.email', 5 | defaultMessage: 'email address invalid', 6 | }, 7 | max: { 8 | id: 'common.form.error.max', 9 | defaultMessage: 'maximum value: {max}', 10 | }, 11 | maxLength: { 12 | id: 'common.form.error.maxLength', 13 | defaultMessage: 'maximum length: {maxLength}', 14 | }, 15 | min: { 16 | id: 'common.form.error.min', 17 | defaultMessage: 'minimum value: {min}', 18 | }, 19 | minLength: { 20 | id: 'common.form.error.minLength', 21 | defaultMessage: 'minimum length: {minLength}', 22 | }, 23 | required: { 24 | id: 'common.form.error.required', 25 | defaultMessage: 'this field is required', 26 | }, 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/containers/ProfileEditForm/ProfileEditForm.validations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | age: { 3 | required: true, 4 | min: 18, 5 | max: 99, 6 | }, 7 | email: { 8 | required: true, 9 | email: true, 10 | validateOnBlur: true, 11 | }, 12 | emailVerified: true, 13 | familyName: { 14 | required: true, 15 | validateOnBlur: true, 16 | minLength: 2, 17 | }, 18 | gender: { 19 | required: true, 20 | validateOnBlur: true, 21 | }, 22 | givenName: { 23 | required: true, 24 | validateOnBlur: true, 25 | minLength: 1, 26 | }, 27 | locale: { 28 | required: true, 29 | validateOnBlur: true, 30 | }, 31 | nickname: { 32 | required: false, 33 | validateOnBlur: true, 34 | maxLength: 10, 35 | }, 36 | notes: { 37 | required: false, 38 | validateOnBlur: true, 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/redux/modules/language/language.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | export const CHANGE_LANGUAGE = '@@language/CHANGE_LANGUAGE'; 3 | export const LOCAL_STORAGE_KEY = 'redux:language'; 4 | export const DEFAULT_LANGUAGE = 'en'; 5 | 6 | type LanguageAction = { 7 | type: string; 8 | language: string; 9 | }; 10 | 11 | function getLanguage() { 12 | const language = localStorage.getItem(LOCAL_STORAGE_KEY); 13 | 14 | return language || DEFAULT_LANGUAGE; 15 | } 16 | 17 | export function changeLanguage(language: string): LanguageAction { 18 | localStorage.setItem(LOCAL_STORAGE_KEY, language); 19 | 20 | return { 21 | type: CHANGE_LANGUAGE, 22 | language, 23 | }; 24 | } 25 | 26 | export function languageReducer(state: string = getLanguage(), action: LanguageAction): string { 27 | return (action.type === CHANGE_LANGUAGE) ? action.language : state; 28 | } 29 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | env: 4 | - NODE_ENV=development NPM_CONFIG_PROGRESS=false 5 | 6 | node_js: 7 | - '5.4' 8 | 9 | script: 10 | - npm run build 11 | 12 | notifications: 13 | slack: 14 | secure: "E0k4IHU8BfYiHnayarcpn4Q13K/bszVkDjeEv6vpTClfoGH8zE/YumMWBqCwEKiKc04K+qqrCZDiSgOK/xyX2xgY+QYb4SKuXvXZda44fgt0B6ogDBwG8LLIJzrJmEd8hu0+eCbql+oIurmvtgZHkJNcZytJV4tLiSkPXLwmJ+9Alqhr3IxzbdfZC//xMC5BVLVly+zCQGe4335DiyuklQms97u5U1ubj0f7kt3MOepQpaiQlQ4CoujIXVZEEO8FJEBl34Q1FtX8MYdPSE0ClgjJh1S7iLdu8ANwPvJJWs5H4KRzyIDDEQgoLvVdZwHAqEUuX69+rCaoDPp/Efk4oJzuXpu14110UlZQp/an6s0kFp0FnkR5T8fOM1NDi91emDYorJMTZlGsVVc2QjlF+dMhyhses25j0j+VnSmAajJ5j9dWK8TaAITnVSGP9b0assSpAEGCvV0rrYNb5JklDBHdRi+QfzKBTsnuJF4WUkW95TiWH4PBpE/0ty7FM2ygU9RG6SGd17srhyTrIw6AhkG7elaXvzCx/GQNLnLSSENP3uRQewjx+vPfqGUWfxyg6WhabNSvIPvOtrpPupHgv+SYUGWQ881gC8ioSSHQwBYinq/p8fHI+xmh1qr5vQaAoGbO6vnT1sdKuLBZ+/kd4i3S4Ubw1tI19pJIf94jgv0=" 15 | -------------------------------------------------------------------------------- /src/containers/HeroPageLayout.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import MainHeader from 'containers/MainHeader/MainHeader'; 4 | import MainFooter from 'components/MainFooter/MainFooter'; 5 | import Spinner from 'components/Spinner/Spinner'; 6 | 7 | const mapStateToProps = ({ spinner }) => ({ spinner }); 8 | const HeroPageLayout = (props) => 9 | ( 10 |
11 | 12 |
13 | {props.children} 14 |
15 | 16 | 20 |
21 | ); 22 | 23 | HeroPageLayout.propTypes = { 24 | spinner: PropTypes.object, 25 | children: PropTypes.element, 26 | }; 27 | 28 | export default connect(mapStateToProps)(HeroPageLayout); 29 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"], 3 | "env": { 4 | "test": { 5 | "presets": ["es2015", "react", "stage-0"], 6 | "plugins": [ 7 | "transform-decorators-legacy", 8 | "transform-runtime", 9 | "add-module-exports" 10 | ] 11 | }, 12 | "development": { 13 | "presets": ["react-hmre", "es2015", "react", "stage-0"], 14 | "plugins": [ 15 | [ "react-transform", { 16 | "transforms": [{ "transform": "react-transform-hmr", "imports": ["react"], "locals": ["module"] }] 17 | }], 18 | "transform-decorators-legacy", 19 | "transform-runtime", 20 | "add-module-exports" 21 | ] 22 | }, 23 | "production": { 24 | "presets": ["es2015", "react", "stage-0"], 25 | "plugins": [ 26 | "transform-decorators-legacy", 27 | "transform-runtime", 28 | "add-module-exports" 29 | ] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/containers/AdminPageLayout.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import MainHeader from 'containers/MainHeader/MainHeader'; 4 | import MainFooter from 'components/MainFooter/MainFooter'; 5 | import Spinner from 'components/Spinner/Spinner'; 6 | import 'styles/admin.scss'; 7 | 8 | const mapStateToProps = ({ spinner }) => ({ spinner }); 9 | const AdminPageLayout = (props) => 10 | ( 11 |
12 | 13 |
14 | {props.children} 15 |
16 | 17 | 21 |
22 | ); 23 | 24 | AdminPageLayout.propTypes = { 25 | spinner: PropTypes.object, 26 | children: PropTypes.element, 27 | }; 28 | 29 | export default connect(mapStateToProps)(AdminPageLayout); 30 | -------------------------------------------------------------------------------- /src/components/UserDropdownMenu/UserDropdownMenu.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { NavDropdown, MenuItem } from 'react-bootstrap'; 4 | import DropdownProfileCard from 'components/DropdownProfileCard/DropdownProfileCard'; 5 | import { links } from 'shared/links'; 6 | 7 | const UserDropdownMenu = (props) => { 8 | const { name, picture, nickname } = props.user; 9 | 10 | return ( 11 | 12 |
  • 13 | 14 |
  • 15 | 16 |
  • 17 | 18 |
  • 19 |
    20 | ); 21 | }; 22 | 23 | UserDropdownMenu.propTypes = { 24 | user: PropTypes.object.isRequired, 25 | logout: PropTypes.func.isRequired, 26 | }; 27 | 28 | export default UserDropdownMenu; 29 | -------------------------------------------------------------------------------- /src/components/FormFields/TextInput.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Element } from 'react'; 3 | import classNames from 'classnames'; 4 | 5 | class TextInput extends React.Component { 6 | static propTypes = { 7 | field: PropTypes.object.isRequired, 8 | children: PropTypes.object, 9 | placeholder: PropTypes.string, 10 | type: PropTypes.string, 11 | }; 12 | 13 | render(): Element { 14 | const inputClasses = classNames({ 15 | 'form-group': true, 16 | 'has-error': this.props.field.invalid, 17 | }); 18 | const type = this.props.type || 'text'; 19 | const { field, placeholder, children } = this.props; 20 | 21 | return ( 22 |
    23 | 29 | { children } 30 |
    31 | ); 32 | } 33 | } 34 | 35 | export default TextInput; 36 | -------------------------------------------------------------------------------- /src/containers/Root.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Provider } from 'react-redux'; 3 | import { Router } from 'react-router'; 4 | 5 | export default class Root extends React.Component { 6 | 7 | static propTypes = { 8 | history: PropTypes.object.isRequired, 9 | store: PropTypes.object.isRequired, 10 | routes: PropTypes.element.isRequired, 11 | }; 12 | 13 | // redux devtools pane 14 | get devTools() { 15 | let returnValue = ; 16 | 17 | if (DEVELOPMENT) { 18 | const DevTools = require('./DevTools'); 19 | 20 | returnValue = ; 21 | } 22 | 23 | return returnValue; 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 |
    30 | 31 | {this.props.routes} 32 | 33 | {this.devTools} 34 |
    35 |
    36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/MainFooter/MainFooter.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/base"; 2 | 3 | .footer { 4 | background-color: $color-footer; 5 | width: 100%; 6 | 7 | ul { 8 | padding: 0; 9 | margin: 0; 10 | text-align: center; 11 | } 12 | } 13 | 14 | .item { 15 | display: inline-block; 16 | margin: 1rem; 17 | 18 | a { 19 | font-size: 1.2rem; 20 | font-weight: bold; 21 | color: #BCBCBC; 22 | text-transform: uppercase; 23 | } 24 | } 25 | .social { 26 | padding: 25px; 27 | a { 28 | text-transform: uppercase; 29 | font-weight: bold; 30 | padding: 10px 30px 10px 55px; 31 | opacity: .3; 32 | text-decoration: none; 33 | color: #373737; 34 | transition: opacity .3s; 35 | 36 | &:hover { 37 | opacity: 1; 38 | } 39 | } 40 | } 41 | 42 | .twitter { 43 | background-image: url('/images/twitter_logo.png'); 44 | text-align: right; 45 | } 46 | 47 | .facebook { 48 | background-image: url('/images/facebook_logo.png'); 49 | text-align: left; 50 | } 51 | -------------------------------------------------------------------------------- /src/redux/modules/auth/auth-reducer.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import { 3 | getState, 4 | LOGIN_SUCCESS, 5 | LOGIN_FAILURE, 6 | LOGOUT_REQUEST, 7 | } from './auth-actions'; 8 | 9 | if (__DEBUG__) { 10 | debug.enable('auth-reducer:*'); 11 | } 12 | 13 | const log = debug('auth-reducer:debug'); 14 | 15 | const authReducer = (state = getState(), action) => { 16 | let newState; 17 | 18 | switch (action.type) { 19 | case LOGIN_SUCCESS: 20 | newState = Object.assign({}, action.state); 21 | break; 22 | case LOGIN_FAILURE: 23 | newState = Object.assign({}, action.state); 24 | break; 25 | case LOGOUT_REQUEST: 26 | newState = Object.assign({}, action.state); 27 | break; 28 | default: 29 | newState = state; 30 | } 31 | 32 | if (newState !== state) { 33 | // only log if state has changed 34 | log('action:', action, 'state:', state, 'newState:', newState); 35 | } 36 | 37 | return newState; 38 | }; 39 | 40 | export default authReducer; 41 | -------------------------------------------------------------------------------- /src/containers/AppContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { IntlProvider } from 'react-intl'; 4 | import DocumentTitle from 'components/DocumentTitle'; 5 | import messages from 'translations'; 6 | import 'styles/app.scss'; 7 | 8 | const mapStateToProps = ({ language, documentTitle }) => ({ language, documentTitle }); 9 | class AppContainer extends Component { 10 | static propTypes = { 11 | language: PropTypes.string.isRequired, 12 | documentTitle: PropTypes.object.isRequired, 13 | children: PropTypes.element.isRequired, 14 | }; 15 | 16 | render() { 17 | const { language, children } = this.props; 18 | 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | } 27 | } 28 | 29 | export default connect(mapStateToProps)(AppContainer); 30 | -------------------------------------------------------------------------------- /src/components/MainFooter/MainFooter.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './MainFooter.scss'; 3 | import { Link } from 'react-router'; 4 | import { FormattedMessage } from 'react-intl'; 5 | import { links } from 'shared/links'; 6 | 7 | const footerLinks = [ 8 | links.aboutUs, 9 | links.faq, 10 | links.policies, 11 | links.terms, 12 | links.help, 13 | ]; 14 | export default class MainFooter extends React.Component { 15 | static propTypes = { 16 | children: React.PropTypes.node, 17 | }; 18 | 19 | render() { 20 | return ( 21 |
    22 | 33 |
    34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/DocumentTitle.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Component } from 'react'; 3 | import { injectIntl } from 'react-intl'; 4 | 5 | type DocumentTitleProps = { 6 | title: Object; 7 | children: any; 8 | intl: { 9 | formatMessage: Function; 10 | }; 11 | }; 12 | 13 | class DocumentTitle extends Component { 14 | static propTypes = { 15 | title: PropTypes.object.isRequired, 16 | children: PropTypes.element.isRequired, 17 | }; 18 | 19 | componentWillMount(): void { 20 | this.updateDocumentTitle(this.props); 21 | } 22 | 23 | componentWillUpdate(newProps: DocumentTitleProps): void { 24 | this.updateDocumentTitle(newProps); 25 | } 26 | 27 | props: DocumentTitleProps; 28 | 29 | updateDocumentTitle(props: DocumentTitleProps): void { 30 | document.title = props.intl.formatMessage(props.title); 31 | } 32 | 33 | render(): React.Element { 34 | return ( 35 |
    36 | {this.props.children} 37 |
    38 | ); 39 | } 40 | } 41 | 42 | export default injectIntl(DocumentTitle); 43 | -------------------------------------------------------------------------------- /src/components/DropdownProfileCard/DropdownProfileCard.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import DropdownProfileCard from './DropdownProfileCard'; 4 | import { Link } from 'react-router'; 5 | 6 | function shallowRender(component) { 7 | const renderer = TestUtils.createRenderer(); 8 | 9 | renderer.render(component); 10 | return renderer.getRenderOutput(); 11 | } 12 | 13 | function shallowRenderWithProps(props = {}) { 14 | return shallowRender(); 15 | } 16 | 17 | describe('[Page] Landing Page', function landingPageSpec() { 18 | beforeEach(() => { 19 | this.props = { 20 | picture: 'picture', 21 | name: 'A User', 22 | nickname: 'a.user', 23 | }; 24 | this.component = shallowRenderWithProps(this.props); 25 | }); 26 | 27 | it('Should render as a ', () => { 28 | expect(this.component.type).to.equal(Link); 29 | }); 30 | 31 | it('Should render 3 children', () => { 32 | expect(this.component.props.children.length).to.equal(3); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/api/user.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import axios from 'axios'; 4 | import type { User } from 'declarations/app'; 5 | 6 | export const getProfile = (): Promise => 7 | axios.get('/api/profile.json').then(response => response.data); 8 | 9 | 10 | export const updateProfile = (user: User): Promise => 11 | // NOTE: this is where the actual `PUT` http request would be called: 12 | // it is mocked here in order to keep things simple (ie, no actual service needs to be built) 13 | // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise 14 | new Promise((resolve) => { 15 | // updated user needs to have given and family names concatenated to produce the `name` property 16 | const name = `${user.givenName} ${user.familyName}`; 17 | const updatedUser = Object.assign({}, user, { name }); 18 | 19 | // insert a short delay to simulate service call delay 20 | setTimeout(() => resolve(updatedUser), 700); 21 | }); 22 | // So to simulate an http request that fails: 23 | // return new Promise((resolve, reject) => reject()); 24 | -------------------------------------------------------------------------------- /src/pages/LandingPage/LandingPageHero.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import config from 'app-config'; 3 | import { Hero, HeroContent, HeroBackground } from 'components/Hero/index'; 4 | import { VAContainer, VAMiddle } from 'components/VAlign/VAlign'; 5 | import styles from './LandingPage.scss'; 6 | 7 | const LandingPageHero = (props) => 8 | ( 9 | 10 | 11 | 12 | 13 | 14 |
    15 |

    16 | {config.name} 17 |

    18 | 19 |

    20 | {config.description} 21 |

    22 |
    23 |
    24 |
    25 |
    26 |
    27 | ); 28 | 29 | LandingPageHero.propTypes = { 30 | backgroundImage: PropTypes.string.isRequired, 31 | }; 32 | 33 | export default LandingPageHero; 34 | -------------------------------------------------------------------------------- /src/components/VAlign/VAlign.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import styles from './VAlign.scss'; 4 | 5 | export const VAMiddle = ({ children = '' }) =>
    {children}
    ; 6 | export const VATop = ({ children = '' }) =>
    {children}
    ; 7 | export const VABottom = ({ children = '' }) =>
    {children}
    ; 8 | 9 | export const VAContainer = ({ horizontal, vertical, children = '' }) => { 10 | const classes = classNames({ 11 | [styles.container]: true, 12 | [styles.vertical]: vertical, 13 | [styles.horizontal]: horizontal, 14 | }); 15 | 16 | return
    {children}
    ; 17 | }; 18 | 19 | VAContainer.propTypes = { 20 | children: React.PropTypes.node, 21 | horizontal: React.PropTypes.bool, 22 | vertical: React.PropTypes.bool, 23 | }; 24 | VAMiddle.propTypes = { 25 | children: React.PropTypes.node, 26 | }; 27 | VATop.propTypes = { 28 | children: React.PropTypes.node, 29 | }; 30 | VABottom.propTypes = { 31 | children: React.PropTypes.node, 32 | }; 33 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*babylon 3 | .*babel 4 | .*npmconf 5 | .*fbjs 6 | .*/node_modules/.*/test/ 7 | 8 | [include] 9 | 10 | [libs] 11 | interfaces/ 12 | 13 | [options] 14 | module.name_mapper='^api/\(.*\)' -> '/src/api/\1' 15 | module.name_mapper='^components/\(.*\)' -> '/src/components/\1' 16 | module.name_mapper='^containers/\(.*\)' -> '/src/containers/\1' 17 | module.name_mapper='^declarations/\(.*\)' -> '/src/declarations/\1' 18 | module.name_mapper='^layouts/\(.*\)' -> '/src/layouts/\1' 19 | module.name_mapper='^pages/\(.*\)' -> '/src/pages/\1' 20 | module.name_mapper='^redux/\(.*\)' -> '/src/redux/\1' 21 | module.name_mapper='^routes/\(.*\)' -> '/src/routes/\1' 22 | module.name_mapper='^shared/\(.*\)' -> '/src/shared/\1' 23 | module.name_mapper='^styles/\(.*\)' -> '/src/styles/\1' 24 | module.name_mapper='^app-config$' -> '/src/app-config' 25 | 26 | module.name_mapper='.*\(.scss\)' -> 'empty/object' 27 | 28 | esproposal.class_static_fields=enable 29 | esproposal.decorators=ignore 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 OpenCredo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/redux/modules/user/user-reducer.js: -------------------------------------------------------------------------------- 1 | import debug from 'debug'; 2 | import { 3 | getUser, 4 | SET_USER, 5 | UPDATE_USER_SUCCESS, 6 | UPDATE_USER_FAILURE, 7 | CLEAR_USER, 8 | } from './user-actions'; 9 | 10 | if (__DEBUG__) { 11 | debug.enable('user-reducer:*'); 12 | } 13 | 14 | const log = debug('user-reducer:debug'); 15 | 16 | const userReducer = (state = getUser(), action) => { 17 | let newState; 18 | 19 | switch (action.type) { 20 | case SET_USER: 21 | newState = Object.assign({}, action.user); 22 | break; 23 | case CLEAR_USER: 24 | newState = null; 25 | break; 26 | case UPDATE_USER_SUCCESS: 27 | newState = Object.assign({}, action.user); 28 | break; 29 | case UPDATE_USER_FAILURE: 30 | // NOTE: this is not essential, but it's useful to explicitly define 31 | newState = state; 32 | break; 33 | default: 34 | newState = state; 35 | } 36 | 37 | if (newState !== state) { 38 | // only log if state has changed 39 | log('action:', action, 'state:', state, 'newState:', newState); 40 | } 41 | 42 | return newState; 43 | }; 44 | 45 | export default userReducer; 46 | -------------------------------------------------------------------------------- /src/components/FormFields/HorizontalRadioGroup.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Element } from 'react'; 3 | 4 | // Support for radio buttons in react-bootstrap is currently lame. 5 | // It's worth watching this though: 6 | // https://github.com/react-bootstrap/react-bootstrap/pull/962 7 | class HorizontalRadioGroup extends React.Component { 8 | static propTypes = { 9 | field: PropTypes.object.isRequired, 10 | values: PropTypes.array.isRequired, 11 | }; 12 | 13 | render(): Element { 14 | const { field, values } = this.props; 15 | 16 | return ( 17 |
    18 | { 19 | values.map(value => 20 | 30 | ) 31 | } 32 |
    33 | ); 34 | } 35 | } 36 | 37 | export default HorizontalRadioGroup; 38 | -------------------------------------------------------------------------------- /FLOWTYPE.md: -------------------------------------------------------------------------------- 1 | # Using flowtype 2 | 3 | This codebase has been set up to use [flowtype](http://flowtype.org/), a static typechecker for Javascript. 4 | 5 | ## Windows installation 6 | 7 | Download and install [the Windows binaries](https://www.ocamlpro.com/pub/ocpwin/flow-builds/). 8 | 9 | ## Running 10 | 11 | You can now run the flow checker: 12 | 13 | ``` sh 14 | npm run flow-check 15 | ``` 16 | 17 | ## Coding 18 | 19 | In order to make use of `flow`'s typechecking, there are several things that need to be in place. 20 | 21 | * put `/* @flow */` as the first line in each file you want flow to use 22 | * check the contents of `.flowconfig`, and make sure you understand how it works: 23 | * the `[libs]` section specifies where custom types are defined 24 | * the `[options]` section specifies: 25 | * mappings for paths used in the code – this means that we can `import` from the project root, rather than defining a path relative to the module we are in 26 | * a mapping that allows us to get around `flow` issues with importing CSS/SCSS files (see [Flowtype and CSS modules](https://gist.github.com/lambdahands/d19e0da96285b749f0ef)) 27 | * ES7 rules 28 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenCredo React + Redux boilerplate 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 | 18 | 19 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/redux/configure-store.js: -------------------------------------------------------------------------------- 1 | import thunk from 'redux-thunk'; 2 | import rootReducer from './root-reducer'; 3 | import { applyMiddleware, compose, createStore } from 'redux'; 4 | import { syncHistory } from 'react-router-redux'; 5 | 6 | function withDevTools(middleware) { 7 | const devTools = window.devToolsExtension 8 | ? window.devToolsExtension() 9 | : require('../containers/DevTools').instrument(); 10 | return compose(middleware, devTools); 11 | } 12 | 13 | export default function configureStore(initialState, browserHistory) { 14 | const routerMiddleware = syncHistory(browserHistory); 15 | 16 | let middleware = applyMiddleware(thunk, routerMiddleware); 17 | 18 | if (__DEBUG__) { 19 | // use devtools in debug environment 20 | middleware = withDevTools(middleware); 21 | } 22 | 23 | const store = middleware(createStore)(rootReducer, initialState); 24 | 25 | if (__DEBUG__) { 26 | // listen for route replays (devtools) 27 | routerMiddleware.listenForReplays(store); 28 | } 29 | 30 | if (module.hot) { 31 | module.hot.accept('./root-reducer', () => { 32 | const nextRootReducer = require('./root-reducer').default; 33 | store.replaceReducer(nextRootReducer); 34 | }); 35 | } 36 | 37 | return store; 38 | } 39 | -------------------------------------------------------------------------------- /src/containers/Sidebar/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import { connect } from 'react-redux'; 4 | 5 | class Sidebar extends Component { 6 | static propTypes = { 7 | dispatch: PropTypes.func, 8 | user: PropTypes.object, 9 | isAdmin: PropTypes.bool, 10 | }; 11 | 12 | render() { 13 | return ( 14 |
      15 |
    • 16 | 17 | Profile 18 | 19 |
    • 20 |
    • 21 | 22 | Profile 23 | 24 |
    • 25 |
    • 26 | 27 | Profile 28 | 29 |
    • 30 |
    • 31 | 32 | Favourites 33 | 34 |
    • 35 |
    36 | ); 37 | } 38 | } 39 | 40 | const mapStateToProps = (state) => ({ 41 | isAuthenticated: state.auth.isAuthenticated, 42 | isAdmin: state.auth.isAdmin, 43 | user: state.user, 44 | }); 45 | 46 | export default connect(mapStateToProps)(Sidebar); 47 | -------------------------------------------------------------------------------- /src/redux/modules/document-title/document-title.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import type { FormattedMessageType } from 'declarations/i18n-types'; 3 | import config from 'app-config'; 4 | 5 | const UPDATE_DOCUMENT_TITLE = '@@document-title/UPDATE'; 6 | const RESET_DOCUMENT_TITLE = '@@document-title/RESET'; 7 | 8 | export type DocumentTitleAction = { 9 | type: string; 10 | documentTitle: FormattedMessageType; 11 | }; 12 | 13 | const initialState: FormattedMessageType = { 14 | id: 'site.name', 15 | defaultMessage: config.name, 16 | }; 17 | 18 | // Action Creators 19 | 20 | export function updateDocumentTitle(documentTitle: FormattedMessageType): DocumentTitleAction { 21 | return { 22 | type: UPDATE_DOCUMENT_TITLE, 23 | documentTitle, 24 | }; 25 | } 26 | 27 | export function resetDocumentTitle(): DocumentTitleAction { 28 | return { 29 | type: RESET_DOCUMENT_TITLE, 30 | documentTitle: initialState, 31 | }; 32 | } 33 | 34 | // Reducer 35 | export function documentTitleReducer( 36 | state: FormattedMessageType = initialState, 37 | action: DocumentTitleAction): FormattedMessageType { 38 | switch (action.type) { 39 | case RESET_DOCUMENT_TITLE: 40 | case UPDATE_DOCUMENT_TITLE: 41 | return action.documentTitle; 42 | default: 43 | return state; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRedirect } from 'react-router'; 3 | import { requireAuthentication as restrict } from 'containers/AuthenticatedComponent'; 4 | 5 | import AppContainer from 'containers/AppContainer'; 6 | import HeroPageLayout from 'containers/HeroPageLayout'; 7 | import AdminPageLayout from 'containers/AdminPageLayout'; 8 | import LandingPage from 'pages/LandingPage/LandingPage'; 9 | import AboutPage from 'pages/AboutPage/AboutPage'; 10 | import ProfileEditPage from 'pages/ProfileEditPage/ProfileEditPage'; 11 | 12 | export default( 13 | // Route components without path will render their children... 14 | 15 | // until a match is found... 16 | 17 | // here 18 | 19 | // Routes without a component will render their children: 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ); 32 | -------------------------------------------------------------------------------- /src/shared/links.js: -------------------------------------------------------------------------------- 1 | import config from 'app-config'; 2 | 3 | export const links = { 4 | home: { 5 | to: '/', 6 | id: 'navigation.home', 7 | description: 'Go to Home / Landing Page', 8 | defaultMessage: config.name, 9 | }, 10 | 11 | aboutUs: { 12 | to: '/pages/about-us', 13 | id: 'navigation.aboutUs', 14 | description: 'About Us page link', 15 | defaultMessage: 'About Us', 16 | }, 17 | 18 | logIn: { 19 | id: 'navigation.logIn', 20 | description: 'Log in', 21 | defaultMessage: 'Log in', 22 | }, 23 | 24 | logOut: { 25 | id: 'navigation.logOut', 26 | description: 'Log out', 27 | defaultMessage: 'Log out', 28 | }, 29 | 30 | faq: { 31 | to: '/pages/faq', 32 | id: 'navigation.faq', 33 | description: 'FAQ', 34 | defaultMessage: 'FAQ', 35 | }, 36 | 37 | policies: { 38 | to: '/pages/policies', 39 | id: 'navigation.policies', 40 | description: 'Policies', 41 | defaultMessage: 'Policies', 42 | }, 43 | 44 | terms: { 45 | to: '/pages/terms', 46 | id: 'navigation.terms', 47 | description: 'Terms & Privacy', 48 | defaultMessage: 'Terms & Privacy', 49 | }, 50 | 51 | help: { 52 | to: '/pages/help', 53 | id: 'navigation.help', 54 | description: 'Help', 55 | defaultMessage: 'Help', 56 | }, 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/FormFields/DropDown.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import DropDown from './DropDown'; 4 | import { Input } from 'react-bootstrap'; 5 | 6 | function shallowRender(component) { 7 | const renderer = TestUtils.createRenderer(); 8 | 9 | renderer.render(component); 10 | 11 | return renderer.getRenderOutput(); 12 | } 13 | 14 | function shallowRenderWithProps(props = {}) { 15 | return shallowRender(); 16 | } 17 | 18 | describe('[FormField] DropDown', function dropDownSpec() { 19 | beforeEach(() => { 20 | this.props = { 21 | label: 'a label', 22 | field: {}, 23 | values: ['one', 'two', 'three', 'four'], 24 | }; 25 | this.component = shallowRenderWithProps(this.props); 26 | }); 27 | 28 | it('Should render as ', () => { 29 | expect(this.component.type).to.equal(Input); 30 | }); 31 | 32 | it('Should render correct number of children', () => { 33 | expect(this.component.props.children.length).to.equal(this.props.values.length); 34 | }); 35 | 36 | it('Should render correct `value` for 2nd child', () => { 37 | expect(this.component.props.children[1].props.value).to.equal(this.props.values[1]); 38 | }); 39 | 40 | it('Should render correct `children` for 3rd child', () => { 41 | expect(this.component.props.children[2].props.children).to.equal(this.props.values[2]); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/pages/AboutPage/AboutPage.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Component } from 'react'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { connect } from 'react-redux'; 5 | import { Grid, Row, Col } from 'react-bootstrap'; 6 | import { HeroBackground, Hero } from 'components/Hero'; 7 | import { messages } from './AboutPage.i18n'; 8 | import { 9 | updateDocumentTitle, 10 | resetDocumentTitle, 11 | } from 'redux/modules/document-title/document-title'; 12 | 13 | class AboutPage extends Component { 14 | static propTypes = { 15 | dispatch: PropTypes.func.isRequired, 16 | }; 17 | 18 | componentDidMount() { 19 | this.props.dispatch(updateDocumentTitle(messages.title)); 20 | } 21 | 22 | componentWillUnmount() { 23 | this.props.dispatch(resetDocumentTitle()); 24 | } 25 | 26 | render() { 27 | return ( 28 |
    29 | 30 | 31 | 32 | 33 | 34 | 35 |

    36 | 37 |

    38 |

    39 | 40 |

    41 | 42 |
    43 |
    44 |
    45 | ); 46 | } 47 | } 48 | 49 | export default connect(() => ({}))(AboutPage); 50 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const webpackConfig = require('./webpack.config'); 3 | 4 | const PRODUCTION = false; 5 | const DEVELOPMENT = false; 6 | const __DEBUG__ = false; 7 | 8 | const karmaWebpackConfig = Object.assign({}, webpackConfig, { 9 | devtool: 'inline-source-map', 10 | resolve: { 11 | modulesDirectories: [ 12 | 'src', 13 | 'node_modules', 14 | ], 15 | extensions: ['', '.json', '.js'], 16 | }, 17 | plugins: [ 18 | new webpack.DefinePlugin({ DEVELOPMENT, PRODUCTION, __DEBUG__ }), 19 | new webpack.optimize.OccurrenceOrderPlugin(), 20 | ], 21 | }); 22 | 23 | module.exports = (karmaConfig) => { 24 | karmaConfig.set({ 25 | 26 | browsers: ['PhantomJS'], 27 | 28 | singleRun: !!process.env.CONTINUOUS_INTEGRATION, 29 | 30 | frameworks: ['mocha', 'sinon', 'chai'], 31 | 32 | files: [ 33 | './node_modules/phantomjs-polyfill/bind-polyfill.js', 34 | 'tests.karma.js', 35 | ], 36 | 37 | preprocessors: { 38 | 'tests.karma.js': ['webpack', 'sourcemap'], 39 | }, 40 | 41 | reporters: ['mocha'], 42 | 43 | plugins: [ 44 | require('karma-webpack'), 45 | require('karma-mocha'), 46 | require('karma-sinon'), 47 | require('karma-chai'), 48 | require('karma-mocha-reporter'), 49 | require('karma-phantomjs-launcher'), 50 | require('karma-sourcemap-loader'), 51 | ], 52 | 53 | webpack: karmaWebpackConfig, 54 | 55 | webpackServer: { 56 | noInfo: true, 57 | quiet: true, 58 | }, 59 | 60 | }); 61 | }; 62 | -------------------------------------------------------------------------------- /src/redux/modules/spinner/spinner.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import debug from 'debug'; 3 | 4 | type SpinnerState = { 5 | canShow: boolean; 6 | messageId: ?string; 7 | }; 8 | 9 | type SpinnerAction = { 10 | type: string; 11 | state: SpinnerState; 12 | }; 13 | 14 | export const SHOW_SPINNER: string = '@@spinner/SHOW_SPINNER'; 15 | export const HIDE_SPINNER: string = '@@spinner/HIDE_SPINNER'; 16 | 17 | export const DEFAULT_SPINNER_STATE: SpinnerState = { 18 | canShow: false, 19 | messageId: null, 20 | }; 21 | 22 | if (__DEBUG__) { 23 | debug.enable('spinner-reducer:*'); 24 | } 25 | 26 | const log = debug('spinner-reducer:debug'); 27 | 28 | export function showSpinner(messageId: string): SpinnerAction { 29 | return { 30 | type: SHOW_SPINNER, 31 | state: { 32 | canShow: true, 33 | messageId, 34 | }, 35 | }; 36 | } 37 | 38 | export function hideSpinner(): SpinnerAction { 39 | return { 40 | type: HIDE_SPINNER, 41 | state: DEFAULT_SPINNER_STATE, 42 | }; 43 | } 44 | 45 | export function spinnerReducer( 46 | state : SpinnerState = DEFAULT_SPINNER_STATE, 47 | action: any): SpinnerState { 48 | let newState: SpinnerState = state; 49 | 50 | switch (action.type) { 51 | case SHOW_SPINNER: 52 | case HIDE_SPINNER: 53 | newState = Object.assign({}, state, action.state); 54 | break; 55 | default: 56 | newState = state; 57 | } 58 | 59 | if (newState !== state) { 60 | // only log if state has changed 61 | log('action:', action, 'state:', state, 'newState:', newState); 62 | } 63 | 64 | return newState; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/FormFields/FormErrorMessages.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Element } from 'react'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import FormMessages from 'redux-form-validation'; 5 | import { messages } from 'shared/forms'; 6 | 7 | const FormErrorMessages = (props: Object): Element => ( 8 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | FormErrorMessages.propTypes = { 35 | field: PropTypes.object.isRequired, 36 | min: PropTypes.number, 37 | max: PropTypes.number, 38 | minLength: PropTypes.number, 39 | maxLength: PropTypes.number, 40 | }; 41 | 42 | export default FormErrorMessages; 43 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const merge = require('lodash/merge'); 3 | 4 | const NODE_ENV = process.env.NODE_ENV || 'development'; 5 | const APP_DIR = 'src'; 6 | const DIST_DIR = 'dist'; 7 | const SERVER_DIR = 'server'; 8 | const STATIC_DIR = 'static'; 9 | const STYLES_DIR = 'styles'; 10 | const PROJECT_ROOT = path.resolve(__dirname, '..'); 11 | 12 | const config = { 13 | env: NODE_ENV, 14 | 15 | paths: { 16 | root: PROJECT_ROOT, 17 | app: path.resolve(PROJECT_ROOT, APP_DIR), 18 | dist: path.resolve(PROJECT_ROOT, DIST_DIR), 19 | server: path.resolve(PROJECT_ROOT, SERVER_DIR), 20 | static: path.resolve(PROJECT_ROOT, APP_DIR, STATIC_DIR), 21 | styles: path.resolve(PROJECT_ROOT, APP_DIR, STYLES_DIR), 22 | entryFile: path.resolve(PROJECT_ROOT, APP_DIR, 'app.js'), 23 | }, 24 | 25 | server: { 26 | hostname: 'localhost', 27 | port: process.env.PORT || 3000, 28 | }, 29 | 30 | webpack: { 31 | vendor: [ 32 | 'react', 33 | 'react-redux', 34 | 'react-router', 35 | 'react-intl', 36 | 'redux', 37 | 'redux-form', 38 | 'react-router-redux', 39 | 'react-bootstrap', 40 | 'react-router-bootstrap', 41 | ], 42 | output: { 43 | publicPath: '/', 44 | }, 45 | }, 46 | 47 | compiler: { 48 | hash_type: 'hash', 49 | stats: { 50 | chunks: false, 51 | chunkModules: false, 52 | colors: true, 53 | }, 54 | }, 55 | }; 56 | 57 | 58 | if (NODE_ENV === 'test') { 59 | module.exports = config; 60 | } else { 61 | module.exports = merge({}, config, require(`./environments/${NODE_ENV}`)); 62 | } 63 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const yargs = require('yargs'); 2 | const express = require('express'); 3 | const webpack = require('webpack'); 4 | const debug = require('debug'); 5 | const config = require('../config'); 6 | const history = require('connect-history-api-fallback'); 7 | const webpackconfig = require('../webpack.config'); 8 | const webpackDevMiddleware = require('webpack-dev-middleware'); 9 | const webpackHotMiddleware = require('webpack-hot-middleware'); 10 | const browserSync = require('browser-sync'); 11 | 12 | debug.enable('app:*'); 13 | 14 | const app = express(); 15 | const compiler = webpack(webpackconfig); 16 | 17 | const log = debug('app:server'); 18 | 19 | log('Redirecting all other requests to index.html'); 20 | app.use(history({ verbose: false })); 21 | 22 | log('Enabling webpack dev middleware.'); 23 | app.use(webpackDevMiddleware(compiler, { 24 | lazy: false, 25 | noInfo: false, 26 | quiet: false, 27 | stats: config.compiler.stats, 28 | })); 29 | 30 | log('Enabling Webpack Hot Module Replacement (HMR).'); 31 | app.use(webpackHotMiddleware(compiler)); 32 | 33 | log(`Serving static content from ${config.paths.static}`); 34 | app.use(express.static(config.paths.static)); 35 | 36 | const port = yargs.argv.port || config.server.port; 37 | app.listen(port, config.server.hostname, () => { 38 | log(`Server is now running at http://${config.server.hostname}:${port}.`); 39 | }); 40 | 41 | if (yargs.argv.withBrowsersync) { 42 | browserSync.init({ 43 | proxy: `${config.server.hostname}:${port}`, 44 | port: 4000, 45 | ui: { 46 | port: 4040, 47 | weinre: { port: 4444 }, 48 | }, 49 | }); 50 | } 51 | 52 | module.exports = app; 53 | -------------------------------------------------------------------------------- /src/containers/AuthenticatedComponent.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { routeActions } from 'react-router-redux'; 4 | import classNames from 'classnames'; 5 | 6 | 7 | const requireAuthentication = (ComposedComponent) => { 8 | const mapStateToProps = (state) => ({ 9 | isAuthenticated: state.auth.isAuthenticated, 10 | }); 11 | 12 | class AuthenticatedComponent extends Component { 13 | static propTypes = { 14 | dispatch: PropTypes.func.isRequired, 15 | isAuthenticated: PropTypes.bool.isRequired, 16 | }; 17 | 18 | componentWillMount() { 19 | this.checkAuth(this.props); 20 | } 21 | 22 | componentWillReceiveProps(props) { 23 | this.checkAuth(props); 24 | } 25 | 26 | checkAuth(props) { 27 | if (!props.isAuthenticated) { 28 | this.redirectToLogin(); 29 | } 30 | } 31 | 32 | redirectToLogin() { 33 | // this would dispatch a notification then a route change. 34 | this.props.dispatch(routeActions.replace({ pathname: '/' })); 35 | } 36 | 37 | render() { 38 | const { isAuthenticated } = this.props; 39 | const wrapperClass = classNames({ 40 | 'is-authenticated': isAuthenticated, 41 | 'requires-authentication': !isAuthenticated, 42 | }); 43 | 44 | return ( 45 |
    46 | { isAuthenticated ? : null } 47 |
    48 | ); 49 | } 50 | } 51 | 52 | return connect(mapStateToProps)(AuthenticatedComponent); 53 | }; 54 | 55 | export { 56 | requireAuthentication, 57 | }; 58 | -------------------------------------------------------------------------------- /src/redux/modules/user/user-actions.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_USER_REQUEST, 3 | UPDATE_USER_SUCCESS, 4 | UPDATE_USER_FAILURE, 5 | updateUserRequest, 6 | updateUserSuccess, 7 | updateUserFailure, 8 | updateUser, 9 | } from './user-actions'; 10 | 11 | describe('updateUserRequest', () => { 12 | it(`returns an action with type ${UPDATE_USER_REQUEST}`, () => { 13 | const action = updateUserRequest(); 14 | expect(action.type).to.be.equal(UPDATE_USER_REQUEST); 15 | }); 16 | 17 | it('does not contain user state', () => { 18 | const action = updateUserRequest(); 19 | expect(action.user).to.be.eq(undefined); 20 | }); 21 | }); 22 | 23 | describe('updateUserSuccess', () => { 24 | it(`returns an action with type ${UPDATE_USER_SUCCESS}`, () => { 25 | const action = updateUserSuccess(); 26 | expect(action.type).to.be.equal(UPDATE_USER_SUCCESS); 27 | }); 28 | 29 | it('contains the updated user', () => { 30 | const user = { name: 'Test' }; 31 | const action = updateUserSuccess(user); 32 | expect(action.user).to.be.equal(user); 33 | }); 34 | }); 35 | 36 | describe('updateUserFailure', () => { 37 | it(`returns an action with type ${UPDATE_USER_FAILURE}`, () => { 38 | const action = updateUserFailure(); 39 | expect(action.type).to.be.equal(UPDATE_USER_FAILURE); 40 | }); 41 | 42 | it('does not contain user state', () => { 43 | const action = updateUserFailure(); 44 | expect(action.user).to.be.eq(undefined); 45 | }); 46 | }); 47 | 48 | describe('updateUser', () => { 49 | it(`is an asynchronous action`, () => { 50 | const action = updateUser(); 51 | expect(action).to.be.a('function'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/containers/LanguageSelectionDropdown/LanguageSelectionDropdown.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { NavDropdown, MenuItem } from 'react-bootstrap'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { changeLanguage } from 'redux/modules/language/language'; 5 | import { connect } from 'react-redux'; 6 | import find from 'lodash/find'; 7 | 8 | const supportedLanguages = [ 9 | { 10 | id: 'lang.en', 11 | key: 'en', 12 | description: 'English', 13 | defaultMessage: 'English', 14 | }, 15 | { 16 | id: 'lang.es', 17 | key: 'es', 18 | description: 'Español', 19 | defaultMessage: 'Español', 20 | }, 21 | ]; 22 | 23 | class LanguageSelectionDropdown extends Component { 24 | 25 | static propTypes = { 26 | dispatch: PropTypes.func.isRequired, 27 | language: PropTypes.string.isRequired, 28 | }; 29 | 30 | languageText(lang) { 31 | return ; 32 | } 33 | 34 | handleLanguageChange = (lang: string) => () => 35 | this.props.dispatch(changeLanguage(lang)); 36 | 37 | render() { 38 | const currentLanguage = find(supportedLanguages, { key: this.props.language }); 39 | 40 | return ( 41 | 42 | {supportedLanguages.map(lang => 43 | 44 | {this.languageText(lang)} 45 | 46 | )} 47 | 48 | ); 49 | } 50 | } 51 | 52 | const mapStateToProps = ({ language }) => ({ language }); 53 | 54 | export default connect(mapStateToProps)(LanguageSelectionDropdown); 55 | -------------------------------------------------------------------------------- /src/containers/ProfileEditForm/ProfileEditForm.i18n.js: -------------------------------------------------------------------------------- 1 | export const messages = { 2 | age: { 3 | placeholder: { 4 | id: 'profile.form.age.placeholder', 5 | defaultMessage: 'age', 6 | }, 7 | }, 8 | email: { 9 | placeholder: { 10 | id: 'profile.form.email.placeholder', 11 | defaultMessage: 'email', 12 | }, 13 | }, 14 | emailVerified: { 15 | label: { 16 | id: 'profile.form.emailVerified.label', 17 | defaultMessage: 'email verified?', 18 | }, 19 | }, 20 | familyName: { 21 | placeholder: { 22 | id: 'profile.form.familyName.placeholder', 23 | defaultMessage: 'family name', 24 | }, 25 | }, 26 | gender: { 27 | male: { 28 | label: { 29 | id: 'profile.form.gender.male.label', 30 | defaultMessage: 'male', 31 | }, 32 | }, 33 | female: { 34 | label: { 35 | id: 'profile.form.gender.female.label', 36 | defaultMessage: 'female', 37 | }, 38 | }, 39 | }, 40 | givenName: { 41 | placeholder: { 42 | id: 'profile.form.givenName.placeholder', 43 | defaultMessage: 'given name', 44 | }, 45 | }, 46 | locale: { 47 | label: { 48 | id: 'profile.form.locale.label', 49 | defaultMessage: 'locale', 50 | }, 51 | }, 52 | nickname: { 53 | placeholder: { 54 | id: 'profile.form.nickname.placeholder', 55 | defaultMessage: 'nickname', 56 | }, 57 | }, 58 | notes: { 59 | label: { 60 | id: 'profile.form.notes.label', 61 | defaultMessage: 'notes', 62 | }, 63 | }, 64 | reset: { 65 | label: { 66 | id: 'profile.form.reset.label', 67 | defaultMessage: 'reset', 68 | }, 69 | }, 70 | save: { 71 | label: { 72 | id: 'profile.form.save.label', 73 | defaultMessage: 'save', 74 | }, 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /src/pages/ProfileEditPage/ProfileEditPage.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import debug from 'debug'; 4 | import React, { PropTypes, Component, Element } from 'react'; 5 | import { FormattedMessage } from 'react-intl'; 6 | import type { User } from 'declarations/app'; 7 | import ProfileEditForm from 'containers/ProfileEditForm/ProfileEditForm'; 8 | import { updateUser } from 'redux/modules/user/user-actions'; 9 | import { autobind } from 'core-decorators'; 10 | import { connect } from 'react-redux'; 11 | import { messages } from './ProfileEditPage.i18n'; 12 | import { 13 | updateDocumentTitle, 14 | resetDocumentTitle, 15 | } from 'redux/modules/document-title/document-title'; 16 | 17 | if (__DEBUG__) { 18 | debug.enable('profile-edit-page:*'); 19 | } 20 | 21 | const log = debug('profile-edit-page:debug'); 22 | 23 | export class ProfileEditPage extends Component { 24 | static propTypes = { 25 | dispatch: PropTypes.func.isRequired, 26 | user: PropTypes.object, 27 | }; 28 | 29 | componentDidMount() { 30 | this.props.dispatch(updateDocumentTitle(messages.title)); 31 | } 32 | 33 | componentWillUnmount() { 34 | this.props.dispatch(resetDocumentTitle()); 35 | } 36 | 37 | @autobind 38 | handleUpdate(user: User) { 39 | log('handleUpdate(): user:', user); 40 | this.props.dispatch(updateUser(user)); 41 | } 42 | 43 | render(): Element { 44 | return ( 45 |
    46 |

    47 | 48 |

    49 | 50 |
    51 | 52 |
    53 | 54 |
    55 | ); 56 | } 57 | } 58 | 59 | const mapStateToProps = (state) => ({ 60 | user: state.user, 61 | }); 62 | 63 | export default connect(mapStateToProps)(ProfileEditPage); 64 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this project 2 | 3 | **Firstly**, thank you getting involved! 4 | 5 | ## What should I know before I get started? 6 | 7 | ### Code of Conduct 8 | 9 | This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). 10 | By participating, you are expected to uphold this code. 11 | 12 | Please report unacceptable behaviour to [github@opencredo.com](mailto:github@opencredo.com). 13 | 14 | ## How Can I Contribute? 15 | 16 | ### Pull Requests 17 | 18 | We :heart: pull requests. 19 | 20 | ### Reporting Bugs 21 | 22 | This section guides you through submitting a bug report. Following these guidelines helps maintainers and the community understand your report :pencil:, reproduce the behaviour :computer: :computer:, and find related reports :mag_right:. 23 | 24 | Before creating bug reports, please perform a search as you might find out that you don't need to create one. 25 | 26 | 27 | ### Before you commit 28 | 29 | Please make sure you do the following before committing code: 30 | 31 | * do a quick smoke-test to ensure the app appears to be functioning as expected 32 | * run `npm run build` – ensure no errors 33 | 34 | 35 | ## Style Guide 36 | 37 | We're mostly following [Airbnb's JavaScript style guide](https://github.com/airbnb/javascript), with a few overrides that you can check in our .eslintrc file. The lint task will catch most of these so be sure to run it (`npm run lint`) on your code. 38 | 39 | 40 | ### Code Conventions 41 | 42 | * We :heart: semicolons `;` 43 | * Commas last `,` 44 | * 2 spaces for indentation (no tabs) 45 | * Prefer single `'` over double `"` quotes. 46 | * Except in JSX. Use double quotes in JSX: `` 47 | * 100 character line length 48 | * Do not use underscore prefix for "private" methods. 49 | * In fact, you probably don't need private methods. Try to keep components small. Compose. 50 | 51 | ### Git commits 52 | 53 | * Use the present tense ("Add feature" not "Added feature") 54 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 55 | * Limit the first line to 72 characters or less 56 | * Reference issues and pull requests liberally ("Fix #17") 57 | 58 | -------------------------------------------------------------------------------- /EDITORS.md: -------------------------------------------------------------------------------- 1 | # Configuring your editor or IDE 2 | 3 | If you're new to React, most editors come with built in support for JavaScript 4 | (and ES2015), but you might need to configure your editor in order to get better 5 | JSX and Flowtype support. Here's a list of recommended packages: 6 | 7 | ## Atom 8 | 9 | * [language-babel](https://atom.io/packages/language-babel): Language plugin for 10 | ES201x, JSX, and Flowtype. This does not support reporting Flow errors; it simply 11 | provides correct syntax highlighting. 12 | * [linter-flow](https://atom.io/packages/linter-flow): Reports flow errors. 13 | * [linter-eslint](https://atom.io/packages/linter-eslint): Reports linting errors. 14 | * [editorconfig](https://atom.io/packages/editorconfig): Adds support for 15 | `.editorconfig` project files. 16 | 17 | As an alternative, [Nuclide](http://nuclide.io) is an Atom package that comes 18 | with built in support for Flow and JSX, but it can be used in conjunction with 19 | the recommended packages above. 20 | 21 | ## Sublime Text 3 22 | 23 | * [babel-sublime](https://packagecontrol.io/packages/Babel) 24 | * [SublimeLinter-flow](https://packagecontrol.io/packages/SublimeLinter-flow) 25 | * [SublimeLinter-contrib-eslint](https://packagecontrol.io/packages/SublimeLinter-contrib-eslint) 26 | * [EditorConfig](https://packagecontrol.io/packages/EditorConfig) 27 | 28 | ## Vim 29 | * [vim-flow](https://github.com/flowtype/vim-flow) 30 | * [vim-jsx](https://github.com/mxw/vim-jsx) 31 | * [syntastic](https://github.com/scrooloose/syntastic) will report linting errors. 32 | * [editorconfig-vim](https://github.com/editorconfig/editorconfig-vim) 33 | 34 | 35 | ## WebStorm / IntelliJ 36 | 37 | The minimum versions required are: 38 | 39 | * WebStorm – version 11 40 | * Intellij – version 15 41 | 42 | Open the project's preferences: 43 | 44 | * Languages & Frameworks 45 | * JavaScript 46 | * **JavaScript language version**: _Flow_ 47 | * Libraries 48 | * tick _ECMAScript 6_ 49 | * tick _HTML_ 50 | * tick _HTML5 / ECMAScript 5_ 51 | * Code Quality Tools 52 | * ESLint 53 | * tick the **Enable** checkbox 54 | * **Node interpreter** – default value should be correct 55 | * **ESLint package** – ditto 56 | * **Configuration file** – _Search for .eslintrc_ 57 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | - The use of sexualized language or imagery 10 | - Personal attacks 11 | - Trolling or insulting/derogatory comments 12 | - Public or private harassment 13 | - Publishing other's private information, such as physical or electronic addresses, without explicit permission 14 | - Other unethical or unprofessional conduct 15 | 16 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 17 | 18 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 19 | 20 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 21 | 22 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at [github@opencredo.com](mailto:github@opencredo.com). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. 23 | 24 | This Code of Conduct is adapted from the Contributor Covenant, version 1.3.0, available from http://contributor-covenant.org/version/1/3/0/ 25 | -------------------------------------------------------------------------------- /src/translations/es.js: -------------------------------------------------------------------------------- 1 | // Spanish translations 2 | export default { 3 | 'aboutPage.title': 'Sobre Nosotros', 4 | 'aboutPage.overview': 'para traducir...', 5 | 'common.form.error.required': 'Este campo es obligatorio', 6 | 'common.form.error.email': 'Correo no válido', 7 | 'common.form.error.max': 'No puede ser más que {max}', 8 | 'common.form.error.maxLength': `No puede tener más de 9 | {maxLength, plural, one {1 carácter} other {{maxLength} caracteres}}`, 10 | 'common.form.error.min': 'Debe ser al menos {min}', 11 | 'common.form.error.minLength': `Debe tener al menos 12 | {minLength, plural, one {1 carácter} other {{minLength} caracteres}}`, 13 | 'landingPage.title': 'Inicio', 14 | 'landingPage.button.clickMe': 'Cliquéame (log)', 15 | 'landingPage.para.pressCtrlH': 'Presione ctrl-h para alternar el panel de Redux Dev Tools.', 16 | 'landingPage.para.autoUpdate': 'Esta página se actualiza automáticamente cuando haces cambios y guardas.', 17 | 'landingPage.para.es7Decorator': `Esta página también demuestra el uso del decorador 18 | @autobind.`, 19 | 'lang.en': 'English', 20 | 'lang.es': 'Español', 21 | 'navigation.aboutUs': 'Sobre Nosotros', 22 | 'navigation.account': 'Cuenta', 23 | 'navigation.faq': 'FAQ', 24 | 'navigation.help': 'Ayuda', 25 | 'navigation.home': 'Opencredo React Boilerplate', 26 | 'navigation.logIn': 'Iniciar Sesión', 27 | 'navigation.logOut': 'Terminar Sesión', 28 | 'navigation.policies': 'Normas', 29 | 'navigation.privacy': 'Privacidad', 30 | 'navigation.terms': 'Términos & Condiciones', 31 | 'profile.edit.title': 'Editar Perfil', 32 | 'profile.form.age.placeholder': 'Edad', 33 | 'profile.form.emailVerified.label': 'Correo verificado', 34 | 'profile.form.email.placeholder': 'correo', 35 | 'profile.form.familyName.placeholder': 'appellido', 36 | 'profile.form.gender.male.label': 'Masculino', 37 | 'profile.form.gender.female.label': 'Femenino', 38 | 'profile.form.givenName.placeholder': 'nombre', 39 | 'profile.form.locale.label': 'Idioma', 40 | 'profile.form.nickname.placeholder': 'nickname', 41 | 'profile.form.notes.label': 'Apuntes', 42 | 'profile.form.reset.label': 'Resetear', 43 | 'profile.form.save.label': 'Guardar', 44 | 'profile.message.updatingUserDetails': 'Actualizando detalles del usuario...', 45 | 'site.message.loggingIn': 'Iniciando sesión...', 46 | 'site.message.loggingOut': 'Terminando sesion...', 47 | 'site.name': 'Opencredo React Boilerplate', 48 | }; 49 | -------------------------------------------------------------------------------- /src/translations/en.js: -------------------------------------------------------------------------------- 1 | // English translations 2 | export default { 3 | 'aboutPage.title': 'About Us', 4 | 'aboutPage.overview': `Currently implemented as a stateless component, 5 | so will not auto-update when changes are made.`, 6 | 'common.form.error.required': 'This field is required', 7 | 'common.form.error.email': 'Email address is invalid', 8 | 'common.form.error.max': 'May not be greater than {max}', 9 | 'common.form.error.maxLength': `May not be more than 10 | {maxLength, plural, one {1 character} other {{maxLength} characters}} long`, 11 | 'common.form.error.min': 'Must be at least {min}', 12 | 'common.form.error.minLength': `Must be at least 13 | {minLength, plural, one {1 character} other {{minLength} characters}} long`, 14 | 'landingPage.title': 'Landing Page', 15 | 'landingPage.button.clickMe': 'Click me (log)', 16 | 'landingPage.para.pressCtrlH': 'Press ctrl-h to toggle Redux Dev Tools (development build only).', 17 | 'landingPage.para.autoUpdate': 'This pages auto-updates when you make changes and save.', 18 | 'landingPage.para.es7Decorator': `This page also demonstrates the use of the 19 | @autobind ES7 decorator.`, 20 | 'lang.en': 'English', 21 | 'lang.es': 'Español', 22 | 'navigation.aboutUs': 'About Us', 23 | 'navigation.account': 'Account', 24 | 'navigation.faq': 'FAQ', 25 | 'navigation.help': 'Help', 26 | 'navigation.home': 'Opencredo React Boilerplate', 27 | 'navigation.logIn': 'Log in', 28 | 'navigation.logOut': 'Log out', 29 | 'navigation.policies': 'Policies', 30 | 'navigation.privacy': 'Privacy', 31 | 'navigation.terms': 'Terms & Conditions', 32 | 'profile.edit.title': 'Edit Profile', 33 | 'profile.form.age.placeholder': 'Age', 34 | 'profile.form.emailVerified.label': 'Email verified', 35 | 'profile.form.email.placeholder': 'email', 36 | 'profile.form.familyName.placeholder': 'family name', 37 | 'profile.form.gender.male.label': 'Male', 38 | 'profile.form.gender.female.label': 'Female', 39 | 'profile.form.givenName.placeholder': 'given name', 40 | 'profile.form.locale.label': 'Locale', 41 | 'profile.form.nickname.placeholder': 'nickname', 42 | 'profile.form.notes.label': 'Notes', 43 | 'profile.form.reset.label': 'Reset', 44 | 'profile.form.save.label': 'Save', 45 | 'profile.message.updatingUserDetails': 'Updating user details...', 46 | 'site.message.loggingIn': 'Logging in...', 47 | 'site.message.loggingOut': 'Logging out...', 48 | 'site.name': 'Opencredo React Boilerplate', 49 | }; 50 | -------------------------------------------------------------------------------- /src/pages/LandingPage/LandingPage.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { FormattedMessage, FormattedHTMLMessage } from 'react-intl'; 3 | import { connect } from 'react-redux'; 4 | import styles from './LandingPage.scss'; 5 | import { Grid, Row, Col, Button } from 'react-bootstrap'; 6 | import debug from 'debug'; 7 | import { autobind } from 'core-decorators'; 8 | import { messages } from './LandingPage.i18n'; 9 | import LandingPageHero from './LandingPageHero'; 10 | import { 11 | updateDocumentTitle, 12 | resetDocumentTitle, 13 | } from 'redux/modules/document-title/document-title'; 14 | 15 | if (__DEBUG__) { 16 | debug.enable('landing-page:*'); 17 | } 18 | 19 | const log = debug('landing-page:info'); 20 | 21 | export class LandingPage extends React.Component { 22 | 23 | static propTypes = { 24 | isAuthenticated: PropTypes.bool, 25 | dispatch: PropTypes.func, 26 | }; 27 | 28 | // executes only on the client 29 | componentDidMount() { 30 | this.props.dispatch(updateDocumentTitle(messages.title)); 31 | } 32 | 33 | componentWillUnmount() { 34 | log('remove custom document title'); 35 | this.props.dispatch(resetDocumentTitle()); 36 | } 37 | 38 | @autobind 39 | handleButtonClick() { 40 | log('button click handler context:', this); 41 | } 42 | 43 | render() { 44 | return ( 45 |
    46 | 47 | 48 | 49 | 50 |

    51 | 52 |

    53 |

    54 | 55 |

    56 |

    57 | 58 |

    59 |

    60 | 61 |

    62 | 63 |
    64 | 65 | 66 | 69 | 70 | 71 |
    72 |
    73 | ); 74 | } 75 | } 76 | 77 | const mapStateToProps = (state) => 78 | ({ isAuthenticated: state.isAuthenticated }); 79 | 80 | export default connect(mapStateToProps)(LandingPage); 81 | -------------------------------------------------------------------------------- /src/components/FormFields/TextInput.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import TextInput from './TextInput'; 4 | 5 | const shallowRender = (component) => { 6 | const renderer = TestUtils.createRenderer(); 7 | 8 | renderer.render(component); 9 | 10 | return renderer.getRenderOutput(); 11 | }; 12 | 13 | const shallowRenderWithProps = (props = {}) => 14 | shallowRender(); 15 | 16 | describe('[FormField] TextInput', function textInputSpec() { 17 | beforeEach(() => { 18 | this.props = { 19 | field: {}, 20 | }; 21 | this.component = shallowRenderWithProps(this.props); 22 | }); 23 | 24 | it('Should render as
    ', () => { 25 | expect(this.component.type).to.equal('div'); 26 | }); 27 | it('Should have no placeholder', () => { 28 | const input = this.component.props.children[0]; 29 | 30 | expect(input.props.placeholder).to.equal(undefined); 31 | }); 32 | it('Should have text type', () => { 33 | const input = this.component.props.children[0]; 34 | 35 | expect(input.props.type).to.equal('text'); 36 | }); 37 | it('Should set class names correctly', () => { 38 | expect(this.component.props.className).to.equal('form-group'); 39 | }); 40 | it('Should have no children', () => { 41 | const secondChild = this.component.props.children[1]; 42 | 43 | expect(secondChild).to.equal(undefined); 44 | }); 45 | 46 | describe('With type', () => { 47 | const type = 'uluru'; 48 | 49 | beforeEach(() => { 50 | this.props.type = type; 51 | this.component = shallowRenderWithProps(this.props); 52 | }); 53 | 54 | it('Should set type', () => { 55 | const input = this.component.props.children[0]; 56 | 57 | expect(input.props.type).to.equal(type); 58 | }); 59 | }); 60 | 61 | describe('With placeholder', () => { 62 | const placeholder = 'holding place'; 63 | 64 | beforeEach(() => { 65 | this.props.placeholder = placeholder; 66 | this.component = shallowRenderWithProps(this.props); 67 | }); 68 | 69 | it('Should set placeholder', () => { 70 | const input = this.component.props.children[0]; 71 | 72 | expect(input.props.placeholder).to.equal(placeholder); 73 | }); 74 | }); 75 | 76 | describe('When field is invalid', () => { 77 | beforeEach(() => { 78 | this.props.field.invalid = true; 79 | this.component = shallowRenderWithProps(this.props); 80 | }); 81 | 82 | it('Should set class names correctly', () => { 83 | expect(this.component.props.className).to.equal('form-group has-error'); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /src/redux/modules/user/user-actions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { User } from 'declarations/app'; 4 | 5 | import { updateProfile } from 'api/user'; 6 | import { showSpinner, hideSpinner } from '../spinner/spinner'; 7 | 8 | export const SET_USER = '@@user/SET_USER'; 9 | export const CLEAR_USER = '@@user/CLEAR_USER'; 10 | export const UPDATE_USER_REQUEST = '@@user/UPDATE_USER_REQUEST'; 11 | export const UPDATE_USER_SUCCESS = '@@user/UPDATE_USER_SUCCESS'; 12 | export const UPDATE_USER_FAILURE = '@@user/UPDATE_USER_FAILURE'; 13 | export const LOCAL_STORAGE_KEY:string = 'redux:user'; 14 | 15 | type UserAction = { 16 | type: string; 17 | user?: ?User; 18 | }; 19 | 20 | const initialUser: User = { 21 | userId: '', 22 | name: '', 23 | givenName: '', 24 | familyName: '', 25 | nickname: '', 26 | picture: '', 27 | email: '', 28 | emailVerified: false, 29 | roles: [], 30 | createdAt: '', 31 | updatedAt: '', 32 | locale: '', 33 | }; 34 | 35 | const persistUser = (user: ?User) => { 36 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(user)); 37 | }; 38 | 39 | export const getUser = (): ?User => { 40 | const storedUser = localStorage.getItem(LOCAL_STORAGE_KEY); 41 | let user: ?User; 42 | 43 | if (storedUser) { 44 | user = JSON.parse(storedUser); 45 | } else { 46 | user = initialUser; 47 | } 48 | 49 | return user; 50 | }; 51 | 52 | export const setUser = (user: ?User): UserAction => { 53 | persistUser(user); 54 | 55 | return { 56 | type: SET_USER, 57 | user, 58 | }; 59 | }; 60 | 61 | export const clearUser = ():UserAction => { 62 | persistUser(null); 63 | 64 | return { 65 | type: CLEAR_USER, 66 | user: undefined, 67 | }; 68 | }; 69 | 70 | export const updateUserRequest = (): UserAction => ({ 71 | type: UPDATE_USER_REQUEST, 72 | }); 73 | 74 | export const updateUserSuccess = (user: User): UserAction => ({ 75 | type: UPDATE_USER_SUCCESS, 76 | user, 77 | }); 78 | 79 | export const updateUserFailure = (): UserAction => ({ 80 | type: UPDATE_USER_FAILURE, 81 | }); 82 | 83 | export const updateUser = (user: User): Function => dispatch => { 84 | dispatch({ 85 | type: UPDATE_USER_REQUEST, 86 | user, 87 | }); 88 | 89 | dispatch(showSpinner('profile.message.updatingUserDetails')); 90 | 91 | updateProfile(user).then( 92 | response => { 93 | dispatch(updateUserSuccess(response)); 94 | dispatch(hideSpinner()); 95 | dispatch(setUser(response)); 96 | }, 97 | () => { 98 | dispatch(updateUserFailure()); 99 | dispatch(hideSpinner()); 100 | } 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /src/containers/MainHeader/MainHeader.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | import { Link } from 'react-router'; 4 | import { Navbar, Nav } from 'react-bootstrap'; 5 | import UserDropdownMenu from 'components/UserDropdownMenu/UserDropdownMenu'; 6 | import { connect } from 'react-redux'; 7 | import { autobind } from 'core-decorators'; 8 | import { loginRequest, logoutRequest } from 'redux/modules/auth/auth-actions'; 9 | import debug from 'debug'; 10 | import LanguageSelectionDropdown from '../LanguageSelectionDropdown/LanguageSelectionDropdown'; 11 | import { links } from 'shared/links'; 12 | 13 | if (__DEBUG__) { 14 | debug.enable('app:*'); 15 | } 16 | 17 | const log = debug('app:main-header'); 18 | 19 | class MainHeader extends React.Component { 20 | static propTypes = { 21 | dispatch: PropTypes.func, 22 | isAuthenticated: PropTypes.bool, 23 | user: PropTypes.object, 24 | }; 25 | 26 | @autobind 27 | onLogin() { 28 | this.props.dispatch(loginRequest()); 29 | } 30 | 31 | @autobind 32 | onLogout() { 33 | this.props.dispatch(logoutRequest()); 34 | } 35 | 36 | willReceiveProps(props) { 37 | log('main-header will receive props', props); 38 | } 39 | 40 | render() { 41 | return ( 42 | 43 | 44 | 45 | 46 | 47 | { /* The above is equivalent to 48 | */ } 51 | 52 | 53 | 54 | 55 | 56 | 73 | 74 | 75 | ); 76 | } 77 | } 78 | 79 | const mapStateToProps = (state) => ({ 80 | isAuthenticated: state.auth.isAuthenticated, 81 | user: state.user, 82 | language: state.language, 83 | }); 84 | 85 | export default connect(mapStateToProps)(MainHeader); 86 | -------------------------------------------------------------------------------- /src/components/FormFields/HorizontalRadioGroup.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TestUtils from 'react-addons-test-utils'; 3 | import HorizontalRadioGroup from './HorizontalRadioGroup'; 4 | 5 | const shallowRender = (component) => { 6 | const renderer = TestUtils.createRenderer(); 7 | 8 | renderer.render(component); 9 | 10 | return renderer.getRenderOutput(); 11 | }; 12 | 13 | const shallowRenderWithProps = (props = {}) => 14 | shallowRender(); 15 | 16 | describe('[FormField] HorizontalRadioGroup', function horizontalRadioGroupSpec() { 17 | beforeEach(() => { 18 | this.props = { 19 | field: { 20 | name: 'Home town', 21 | value: 'perth', 22 | }, 23 | values: [ 24 | { 25 | label: 'Perth', 26 | value: 'perth', 27 | }, 28 | { 29 | label: 'Melbourne', 30 | value: 'melbourne', 31 | }, 32 | { 33 | label: 'Sydney', 34 | value: 'sydney', 35 | }, 36 | ], 37 | }; 38 | this.component = shallowRenderWithProps(this.props); 39 | }); 40 | 41 | it('Should render as
    ', () => { 42 | expect(this.component.type).to.equal('div'); 43 | }); 44 | 45 | it('Should render correct number of children', () => { 46 | expect(this.component.props.children.length).to.equal(this.props.values.length); 47 | }); 48 | 49 | it('Should render label correctly for 2nd item', () => { 50 | const index = 1; 51 | const child = this.component.props.children[index]; 52 | const label = child.props.children[1]; 53 | 54 | expect(label).to.equal(this.props.values[index].label); 55 | }); 56 | 57 | it('Should set value correctly for 3rd item', () => { 58 | const index = 2; 59 | const child = this.component.props.children[index]; 60 | const input = child.props.children[0]; 61 | const value = input.props.value; 62 | 63 | expect(value).to.equal(this.props.values[index].value); 64 | }); 65 | 66 | it('Should set checked correctly for 1st item', () => { 67 | const index = 0; 68 | const child = this.component.props.children[index]; 69 | const input = child.props.children[0]; 70 | const checked = input.props.checked; 71 | const value = input.props.value; 72 | const expectedChecked = (value === this.props.field.value); 73 | 74 | expect(checked).to.equal(expectedChecked); 75 | }); 76 | 77 | it('Should set checked correctly for 2nd item', () => { 78 | const index = 1; 79 | const child = this.component.props.children[index]; 80 | const input = child.props.children[0]; 81 | const checked = input.props.checked; 82 | const value = input.props.value; 83 | const expectedChecked = (value === this.props.field.value); 84 | 85 | expect(checked).to.equal(expectedChecked); 86 | }); 87 | 88 | it('Should set checked correctly for 3rd item', () => { 89 | const index = 2; 90 | const child = this.component.props.children[index]; 91 | const input = child.props.children[0]; 92 | const checked = input.props.checked; 93 | const value = input.props.value; 94 | const expectedChecked = (value === this.props.field.value); 95 | 96 | expect(checked).to.equal(expectedChecked); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/redux/modules/auth/auth-actions.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { getProfile } from 'api/user'; 4 | import { setUser, clearUser } from '../user/user-actions'; 5 | import { showSpinner, hideSpinner } from '../spinner/spinner'; 6 | 7 | export const LOGIN_REQUEST = '@@auth/LOGIN_REQUEST'; 8 | export const LOGIN_SUCCESS = '@@auth/LOGIN_SUCCESS'; 9 | export const LOGIN_FAILURE = '@@auth/LOGIN_FAILURE'; 10 | export const LOGOUT_REQUEST = '@@auth/LOGOUT_REQUEST'; 11 | export const LOGOUT_SUCCESS = '@@auth/LOGOUT_SUCCESS'; 12 | export const LOCAL_STORAGE_KEY = 'redux:auth'; 13 | 14 | type AuthState = { 15 | isLoading: boolean; 16 | isAuthenticated: boolean; 17 | isAdmin: boolean; 18 | token: ?string; 19 | }; 20 | 21 | type AuthAction = { 22 | type: string; 23 | state: ?AuthState; 24 | }; 25 | 26 | const initialState = { 27 | isLoading: true, 28 | isAuthenticated: false, 29 | isAdmin: false, 30 | token: null, 31 | }; 32 | 33 | const loginRequestAction: AuthAction = { 34 | type: LOGIN_REQUEST, 35 | state: initialState, 36 | }; 37 | 38 | const persistState = (state: ?AuthState) => { 39 | localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(state)); 40 | }; 41 | 42 | export const getState = (): AuthState => { 43 | const storedState = localStorage.getItem(LOCAL_STORAGE_KEY); 44 | let state: ?AuthState; 45 | 46 | if (storedState) { 47 | state = JSON.parse(storedState); 48 | } else { 49 | state = initialState; 50 | } 51 | 52 | return state; 53 | }; 54 | 55 | export const loginSuccess = (): AuthAction => { 56 | const state = { 57 | isLoading: false, 58 | isAuthenticated: true, 59 | isAdmin: true, 60 | token: 'eyJ0eXAasdfiOi', 61 | }; 62 | 63 | persistState(state); 64 | 65 | return { 66 | type: LOGIN_SUCCESS, 67 | state, 68 | }; 69 | }; 70 | 71 | export const loginFailure = (): AuthAction => { 72 | persistState(initialState); 73 | 74 | return { 75 | type: LOGIN_FAILURE, 76 | state: initialState, 77 | }; 78 | }; 79 | 80 | export const loginRequest = (): Function => 81 | // Returning a function works because `redux-thunk` middleware is installed: 82 | // https://github.com/gaearon/redux-thunk 83 | // See `configure-store.js`. 84 | dispatch => { 85 | dispatch(loginRequestAction); 86 | dispatch(showSpinner('site.message.loggingIn')); 87 | 88 | getProfile().then( 89 | response => { 90 | // insert a short delay to simulate service call delay - remove in real application 91 | setTimeout(() => { 92 | dispatch(loginSuccess(response)); 93 | dispatch(hideSpinner()); 94 | dispatch(setUser(response)); 95 | }, 700); 96 | }, 97 | () => { 98 | dispatch(loginFailure()); 99 | dispatch(hideSpinner()); 100 | dispatch(clearUser()); 101 | } 102 | ); 103 | }; 104 | 105 | export const logoutRequest = (): Function => dispatch => { 106 | dispatch({ 107 | type: LOGOUT_REQUEST, 108 | }); 109 | dispatch(showSpinner('site.message.loggingOut')); 110 | 111 | // insert a short delay to simulate service call delay - remove in real application 112 | setTimeout(() => { 113 | persistState(initialState); 114 | dispatch(clearUser()); 115 | dispatch(hideSpinner()); 116 | dispatch({ 117 | type: LOGOUT_SUCCESS, 118 | state: initialState, 119 | }); 120 | }, 700); 121 | }; 122 | -------------------------------------------------------------------------------- /src/styles/vendor/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ 2 | html { 3 | font-family: sans-serif; 4 | -webkit-text-size-adjust: 100%; 5 | -ms-text-size-adjust: 100%; 6 | } 7 | body { 8 | margin: 0; 9 | } 10 | article, 11 | aside, 12 | details, 13 | figcaption, 14 | figure, 15 | footer, 16 | header, 17 | hgroup, 18 | main, 19 | menu, 20 | nav, 21 | section, 22 | summary { 23 | display: block; 24 | } 25 | audio, 26 | canvas, 27 | progress, 28 | video { 29 | display: inline-block; 30 | vertical-align: baseline; 31 | } 32 | audio:not([controls]) { 33 | display: none; 34 | height: 0; 35 | } 36 | [hidden], 37 | template { 38 | display: none; 39 | } 40 | a { 41 | background-color: transparent; 42 | } 43 | a:active, 44 | a:hover { 45 | outline: 0; 46 | } 47 | abbr[title] { 48 | border-bottom: 1px dotted; 49 | } 50 | b, 51 | strong { 52 | font-weight: bold; 53 | } 54 | dfn { 55 | font-style: italic; 56 | } 57 | h1 { 58 | margin: .67em 0; 59 | font-size: 2em; 60 | } 61 | mark { 62 | color: #000; 63 | background: #ff0; 64 | } 65 | small { 66 | font-size: 80%; 67 | } 68 | sub, 69 | sup { 70 | position: relative; 71 | font-size: 75%; 72 | line-height: 0; 73 | vertical-align: baseline; 74 | } 75 | sup { 76 | top: -.5em; 77 | } 78 | sub { 79 | bottom: -.25em; 80 | } 81 | img { 82 | border: 0; 83 | } 84 | svg:not(:root) { 85 | overflow: hidden; 86 | } 87 | figure { 88 | margin: 1em 40px; 89 | } 90 | hr { 91 | height: 0; 92 | -webkit-box-sizing: content-box; 93 | -moz-box-sizing: content-box; 94 | box-sizing: content-box; 95 | } 96 | pre { 97 | overflow: auto; 98 | } 99 | code, 100 | kbd, 101 | pre, 102 | samp { 103 | font-family: monospace, monospace; 104 | font-size: 1em; 105 | } 106 | button, 107 | input, 108 | optgroup, 109 | select, 110 | textarea { 111 | margin: 0; 112 | font: inherit; 113 | color: inherit; 114 | } 115 | button { 116 | overflow: visible; 117 | } 118 | button, 119 | select { 120 | text-transform: none; 121 | } 122 | button, 123 | html input[type="button"], 124 | input[type="reset"], 125 | input[type="submit"] { 126 | -webkit-appearance: button; 127 | cursor: pointer; 128 | } 129 | button[disabled], 130 | html input[disabled] { 131 | cursor: default; 132 | } 133 | button::-moz-focus-inner, 134 | input::-moz-focus-inner { 135 | padding: 0; 136 | border: 0; 137 | } 138 | input { 139 | line-height: normal; 140 | } 141 | input[type="checkbox"], 142 | input[type="radio"] { 143 | -webkit-box-sizing: border-box; 144 | -moz-box-sizing: border-box; 145 | box-sizing: border-box; 146 | padding: 0; 147 | } 148 | input[type="number"]::-webkit-inner-spin-button, 149 | input[type="number"]::-webkit-outer-spin-button { 150 | height: auto; 151 | } 152 | input[type="search"] { 153 | -webkit-box-sizing: content-box; 154 | -moz-box-sizing: content-box; 155 | box-sizing: content-box; 156 | -webkit-appearance: textfield; 157 | } 158 | input[type="search"]::-webkit-search-cancel-button, 159 | input[type="search"]::-webkit-search-decoration { 160 | -webkit-appearance: none; 161 | } 162 | fieldset { 163 | padding: .35em .625em .75em; 164 | margin: 0 2px; 165 | border: 1px solid #c0c0c0; 166 | } 167 | legend { 168 | padding: 0; 169 | border: 0; 170 | } 171 | textarea { 172 | overflow: auto; 173 | } 174 | optgroup { 175 | font-weight: bold; 176 | } 177 | table { 178 | border-spacing: 0; 179 | border-collapse: collapse; 180 | } 181 | td, 182 | th { 183 | padding: 0; 184 | } 185 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opencredo-react-boilerplate", 3 | "version": "1.0.2", 4 | "description": "", 5 | "main": "index.js", 6 | "engines": { 7 | "node": ">=5.3.0", 8 | "npm": "^3.0.0" 9 | }, 10 | "scripts": { 11 | "build": "npm run test && npm run dist:clean && npm run copy:static && npm run compile", 12 | "dist:clean": "rm -rf dist && mkdir dist", 13 | "copy:static": "cp -r src/static/* dist", 14 | "compile": "NODE_ENV=production webpack -p --bail", 15 | "dev": "node --harmony server", 16 | "dev:bs": "node --harmony server --with-browsersync", 17 | "flow-check": "flow check", 18 | "karma": "NODE_ENV=test karma start --single-run", 19 | "lint": "eslint . ./", 20 | "start": "npm run dev", 21 | "start:bs": "npm run dev:bs", 22 | "test": "npm run flow-check && npm run lint && npm run karma" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/opencredo/opencredo-react-boilerplate.git" 27 | }, 28 | "author": "Matt Calthrop (mcalthrop)", 29 | "contributors": [ 30 | { 31 | "name": "Vincent Martinez", 32 | "email": "eykosioux@gmail.com", 33 | "username": "eyko" 34 | } 35 | ], 36 | "license": "MIT", 37 | "dependencies": { 38 | "axios": "^0.9.0", 39 | "classnames": "^2.2.3", 40 | "empty": "^0.10.1", 41 | "lodash": "^4.5.1", 42 | "react": "^0.14.6", 43 | "react-bootstrap": "^0.28.2", 44 | "react-dom": "^0.14.6", 45 | "react-intl": "2.0.0-rc-1", 46 | "react-redux": "^4.0.6", 47 | "react-router": "^2.0.0", 48 | "react-router-bootstrap": "^0.20.1", 49 | "react-router-redux": "^2.1.0", 50 | "redux": "^3.3.1", 51 | "redux-form": "^4.1.8", 52 | "redux-form-validation": "^0.0.6", 53 | "redux-logger": "^2.3.2", 54 | "redux-thunk": "^1.0.3", 55 | "url": "^0.11.0" 56 | }, 57 | "devDependencies": { 58 | "babel-core": "^6.4.0", 59 | "babel-eslint": "^5.0.0", 60 | "babel-loader": "^6.2.1", 61 | "babel-plugin-add-module-exports": "^0.1.2", 62 | "babel-plugin-react-transform": "^2.0.0", 63 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 64 | "babel-plugin-transform-runtime": "^6.6.0", 65 | "babel-preset-es2015": "^6.6.0", 66 | "babel-preset-react": "^6.5.0", 67 | "babel-preset-react-hmre": "^1.1.0", 68 | "babel-preset-stage-0": "^6.5.0", 69 | "babel-register": "^6.6.0", 70 | "babel-runtime": "^6.5.0", 71 | "browser-sync": "^2.11.1", 72 | "chai": "^3.5.0", 73 | "connect-history-api-fallback": "^1.1.0", 74 | "core-decorators": "^0.11.0", 75 | "css-loader": "^0.23.1", 76 | "cssnano": "^3.5.2", 77 | "eslint": "2.2.0", 78 | "eslint-config-airbnb": "6.0.2", 79 | "eslint-loader": "^1.3.0", 80 | "eslint-plugin-babel": "^3.1.0", 81 | "eslint-plugin-react": "^4.1.0", 82 | "eventsource-polyfill": "^0.9.6", 83 | "express": "^4.13.3", 84 | "extract-text-webpack-plugin": "^1.0.1", 85 | "file-loader": "^0.8.5", 86 | "flow-bin": "^0.22.1", 87 | "fs-extra": "^0.26.4", 88 | "html-webpack-plugin": "^2.9.0", 89 | "isparta-loader": "^2.0.0", 90 | "json-loader": "^0.5.4", 91 | "karma": "^0.13.19", 92 | "karma-chai": "^0.1.0", 93 | "karma-mocha": "^0.2.1", 94 | "karma-mocha-reporter": "^1.2.3", 95 | "karma-phantomjs-launcher": "^1.0.0", 96 | "karma-sinon": "^1.0.4", 97 | "karma-sourcemap-loader": "^0.3.7", 98 | "karma-webpack": "^1.7.0", 99 | "mocha": "^2.4.5", 100 | "node-sass": "^3.4.2", 101 | "phantomjs-polyfill": "0.0.2", 102 | "phantomjs-prebuilt": "^2.1.3", 103 | "postcss-loader": "^0.8.0", 104 | "react-addons-test-utils": "^0.14.6", 105 | "react-transform-hmr": "^1.0.1", 106 | "redux-devtools": "^3.1.1", 107 | "redux-devtools-dock-monitor": "^1.0.1", 108 | "redux-devtools-log-monitor": "^1.0.2", 109 | "sass-loader": "^3.1.2", 110 | "sinon": "^1.17.2", 111 | "style-loader": "^0.13.0", 112 | "url-loader": "^0.5.7", 113 | "webpack": "^1.12.11", 114 | "webpack-dev-middleware": "^1.5.1", 115 | "webpack-hot-middleware": "^2.7.1" 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const head = require('lodash/head'); 2 | const tail = require('lodash/tail'); 3 | const path = require('path'); 4 | const webpack = require('webpack'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 7 | const cssnano = require('cssnano'); 8 | const debug = require('debug'); 9 | const config = require('./config'); 10 | 11 | debug.enable('app:*'); 12 | 13 | const log = debug('app:webpack'); 14 | 15 | // Environment 16 | const NODE_ENV = process.env.NODE_ENV || 'development'; 17 | const DEVELOPMENT = NODE_ENV === 'development'; 18 | const TESTING = NODE_ENV === 'test'; 19 | const PRODUCTION = NODE_ENV === 'production'; 20 | const __DEBUG__ = DEVELOPMENT; 21 | 22 | 23 | // Webpack configuration 24 | log('Creating webpack configuration...'); 25 | const webpackconfig = { 26 | devtool: config.webpack.devtool, 27 | resolve: { 28 | root: config.paths.app, 29 | extensions: ['', '.js', '.jsx'], 30 | }, 31 | 32 | entry: { 33 | app: [config.paths.entryFile], 34 | vendor: config.webpack.vendor, 35 | }, 36 | 37 | output: { 38 | filename: `[name].[${config.compiler.hash_type}].js`, 39 | path: config.paths.dist, 40 | publicPath: config.webpack.output.publicPath, 41 | }, 42 | 43 | plugins: [ 44 | new webpack.DefinePlugin({ DEVELOPMENT, PRODUCTION, __DEBUG__ }), 45 | new webpack.optimize.OccurrenceOrderPlugin(), 46 | new HtmlWebpackPlugin({ 47 | template: path.resolve(config.paths.app, 'index.html'), 48 | hash: true, 49 | favicon: path.resolve(config.paths.static, 'favicon.png'), 50 | filename: 'index.html', 51 | inject: 'body', 52 | minify: { 53 | collapseWhitespace: true, 54 | }, 55 | }), 56 | ], 57 | 58 | module: { 59 | preLoaders: [ 60 | { 61 | test: /\.jsx?/, 62 | loader: 'eslint-loader', 63 | exclude: /node_modules/, 64 | }, 65 | ], 66 | loaders: [ 67 | { 68 | test: /\.jsx?/, 69 | loaders: ['babel-loader'], 70 | include: config.paths.app, 71 | }, 72 | { 73 | test: /\.json$/, 74 | loader: 'json', 75 | }, 76 | // Any .scss file in ./src/... *except* those in ./src/styles/ 77 | // are local css modules. the class names and ids will be changed to: 78 | // [name]-[local]-[hash:base64:5] 79 | { 80 | test: /\.scss$/, 81 | include: /src\/(?!styles).+/, 82 | loaders: [ 83 | 'style', 84 | 'css?modules&sourceMap&importLoaders=1&localIdentName=[name]-[local]-[hash:base64:5]', 85 | 'postcss', 86 | 'sass', 87 | ], 88 | }, 89 | // Any .scss files in ./src/styles are treated as normal (not local) 90 | // sass files, and so class names and ids will remain as specified 91 | { 92 | test: /\.scss$/, 93 | include: /src\/styles/, 94 | loader: 'style!css?sourceMap!postcss!sass', 95 | }, 96 | // File loaders 97 | /* eslint-disable */ 98 | { test: /\.woff(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff' }, 99 | { test: /\.woff2(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/font-woff2' }, 100 | { test: /\.ttf(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=application/octet-stream' }, 101 | { test: /\.eot(\?.*)?$/, loader: 'file?prefix=fonts/&name=[path][name].[ext]' }, 102 | { test: /\.svg(\?.*)?$/, loader: 'url?prefix=fonts/&name=[path][name].[ext]&limit=10000&mimetype=image/svg+xml' }, 103 | { test: /\.(png|jpg)$/, loader: 'url?limit=8192' }, 104 | /* eslint-enable */ 105 | ], 106 | }, 107 | 108 | postcss: [ 109 | cssnano({ 110 | sourcemap: true, 111 | autoprefixer: { 112 | add: true, 113 | remove: true, 114 | browsers: ['last 2 versions'], 115 | }, 116 | safe: true, 117 | discardComments: { 118 | removeAll: true, 119 | }, 120 | }), 121 | ], 122 | 123 | sassLoader: { 124 | includePaths: config.paths.styles, 125 | }, 126 | 127 | eslint: { 128 | configFile: path.resolve(config.paths.root, '.eslintrc'), 129 | }, 130 | }; 131 | 132 | if (!TESTING) { 133 | webpackconfig.plugins.push(new webpack.optimize.CommonsChunkPlugin({ 134 | names: ['vendor'], 135 | })); 136 | } 137 | 138 | if (DEVELOPMENT) { 139 | log('Extending webpack configuration with development settings.'); 140 | 141 | log('Adding HMR entry points'); 142 | webpackconfig.entry.app.push('webpack-hot-middleware/client'); 143 | 144 | log('Enable development plugins (HMR, NoErrors)'); 145 | webpackconfig.plugins.push( 146 | new webpack.HotModuleReplacementPlugin(), 147 | new webpack.NoErrorsPlugin() 148 | ); 149 | } 150 | 151 | 152 | if (PRODUCTION) { 153 | log('Extending webpack configuration with production settings.'); 154 | 155 | log('Add uglify and dedupe plugins'); 156 | webpackconfig.plugins.push( 157 | new webpack.optimize.UglifyJsPlugin({ 158 | compress: { 159 | unused: true, 160 | dead_code: true, 161 | }, 162 | }), 163 | new webpack.optimize.DedupePlugin() 164 | ); 165 | 166 | log('Apply ExtractTextPlugin to CSS loaders.'); 167 | webpackconfig.module.loaders.filter(loader => 168 | loader.loaders && loader.loaders.find(name => /css/.test(name.split('?')[0])) 169 | ).forEach(loader => { 170 | /* eslint-disable */ 171 | const first = head(loader.loaders); 172 | const rest = tail(loader.loaders); 173 | loader.loader = ExtractTextPlugin.extract(first, rest.join('!')); 174 | delete loader.loaders; 175 | /* eslint-enable */ 176 | }); 177 | webpackconfig.plugins.push( 178 | new ExtractTextPlugin('[name].[contenthash].css', { 179 | allChunks: true, 180 | }) 181 | ); 182 | } 183 | 184 | module.exports = webpackconfig; 185 | -------------------------------------------------------------------------------- /src/containers/ProfileEditForm/ProfileEditForm.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import React, { PropTypes, Element } from 'react'; 3 | import { Button, Input, Row, Col } from 'react-bootstrap'; 4 | import { FormattedMessage, intlShape, injectIntl } from 'react-intl'; 5 | import { generateValidation } from 'redux-form-validation'; 6 | import TextInput from 'components/FormFields/TextInput'; 7 | import HorizontalRadioGroup from 'components/FormFields/HorizontalRadioGroup'; 8 | import DropDown from 'components/FormFields/DropDown'; 9 | import FormErrorMessages from 'components/FormFields/FormErrorMessages'; 10 | import validations from './ProfileEditForm.validations'; 11 | import { reduxForm } from 'redux-form'; 12 | import { messages } from './ProfileEditForm.i18n'; 13 | import styles from './ProfileEditForm.scss'; 14 | import { autobind } from 'core-decorators'; 15 | 16 | const MALE: string = 'male'; 17 | const FEMALE: string = 'female'; 18 | 19 | class ProfileEditForm extends React.Component { 20 | static propTypes = { 21 | fields: PropTypes.object.isRequired, 22 | intl: intlShape.isRequired, 23 | values: PropTypes.object.isRequired, 24 | pristine: PropTypes.bool.isRequired, 25 | invalid: PropTypes.bool.isRequired, 26 | resetForm: PropTypes.func.isRequired, 27 | user: PropTypes.object.isRequired, 28 | handleUpdate: PropTypes.func.isRequired, 29 | }; 30 | 31 | @autobind 32 | onUpdateClick() { 33 | this.props.handleUpdate(Object.assign({}, this.props.user, this.props.values)); 34 | } 35 | 36 | @autobind 37 | onResetClick() { 38 | this.props.resetForm(); 39 | } 40 | 41 | isUpdateButtonDisabled(): boolean { 42 | return this.props.pristine || this.props.invalid; 43 | } 44 | 45 | isResetButtonDisabled(): boolean { 46 | return this.props.pristine; 47 | } 48 | 49 | render(): Element { 50 | const { 51 | fields: { 52 | givenName, 53 | familyName, 54 | nickname, 55 | email, 56 | emailVerified, 57 | age, 58 | gender, 59 | locale, 60 | notes, 61 | }, 62 | } = this.props; 63 | const { formatMessage } = this.props.intl; 64 | const genderValues = [ 65 | { 66 | label: formatMessage(messages.gender.male.label), 67 | value: MALE, 68 | }, 69 | { 70 | label: formatMessage(messages.gender.female.label), 71 | value: FEMALE, 72 | }, 73 | ]; 74 | 75 | // in a real app, the locales would be populated via service call: 76 | const locales: string[] = ['en-GB', 'en-AU', 'es-ES', 'es-CR', 'es-NI']; 77 | 78 | return ( 79 |
    80 |
    81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |   125 | { 126 | // Need to include the preceding non-breaking space, because when React renders the HTML, 127 | // there is no gap in the markup between the buttons - which results in the buttons being 128 | // flush up against each other. 129 | } 130 | 137 | 138 | 139 |
    140 |
    141 | ); 142 | } 143 | } 144 | 145 | const reduxFormConfig: Object = { 146 | form: 'editProfile', 147 | ...generateValidation(validations), 148 | }; 149 | 150 | const mapStateToProps = (state, props) => ({ initialValues: props.user }); 151 | 152 | export default reduxForm(reduxFormConfig, mapStateToProps)(injectIntl(ProfileEditForm)); 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # opencredo-react-boilerplate 2 | 3 | [![Build Status](https://travis-ci.org/opencredo/opencredo-react-boilerplate.svg?branch=master)](https://travis-ci.org/opencredo/opencredo-react-boilerplate) 4 | [![Dependency Status](https://david-dm.org/opencredo/opencredo-react-boilerplate.svg)](https://david-dm.org/opencredo/opencredo-react-boilerplate) 5 | [![devDependency Status](https://david-dm.org/opencredo/opencredo-react-boilerplate/dev-status.svg)](https://david-dm.org/opencredo/opencredo-react-boilerplate#info=devDependencies) 6 | 7 | React and Redux boilerplate codebase. 8 | 9 | ## Features 10 | 11 | * [`react`](https://facebook.github.io/react/) 12 | * [`redux`](http://rackt.org/redux/) 13 | * [`react-router`](https://github.com/rackt/react-router) 14 | * [`react-router-redux`](https://github.com/rackt/react-router-redux) – synchronises react router state with your redux store 15 | * [`webpack`](https://webpack.github.io) 16 | - Configured with [`webpack-dev-middleware`](https://github.com/webpack/webpack-dev-middleware) and [`webpack-hot-middleware`](https://github.com/glenjamin/webpack-hot-middleware) 17 | - Hot Module Replacement with [`react-transform-hmr`](https://github.com/gaearon/react-transform-hmr) and [`babel-preset-react-hmre`](https://github.com/danmartinez101/babel-preset-react-hmre) 18 | - SASS / SCSS 19 | - [CSS modules](https://github.com/css-modules/css-modules) 20 | - `.eslintrc` pre-loader 21 | - Support for production and development builds based on `NODE_ENV` 22 | * [`react-addons-test-utils`](https://facebook.github.io/react/docs/test-utils.html) for unit testing 23 | * [Flowtype](http://flowtype.org) Static type checker (see [FLOWTYPE.md](./FLOWTYPE.md) for more info) 24 | * [Babel](https://babeljs.io) for ES2015 and _beyond_ 25 | - Presets: `es2015`, `react`, `stage-0`, and `react-hmre` (development) 26 | * [`react-bootstrap`](https://react-bootstrap.github.io) and [`react-router-bootstrap`](https://github.com/react-bootstrap/react-router-bootstrap) 27 | * [`react-intl`](https://github.com/yahoo/react-intl/) for i18n support with several example translations; using v2 beta: keep an eye on the [RFC](https://github.com/yahoo/react-intl/issues/162) 28 | * [`redux-form`](https://github.com/erikras/redux-form) and [`redux-form-validation`](https://github.com/CosticaPuntaru/redux-form-validation) for form integration and validation 29 | * Example production build script (`npm run build`) 30 | * Basic support for restricted pages. Check `src/routes.js` for examples of restricted routes 31 | 32 | ## Usage 33 | 34 | ### First time 35 | 36 | Clone this repo, then run `npm install`. 37 | 38 | Windows users need to follow the instructions in [FLOWTYPE.md](./FLOWTYPE.md#windows-installation) to install the `flow` binaries. 39 | 40 | ### Development 41 | 42 | When you're developing, start webpack as follows: 43 | 44 | ``` sh 45 | npm run start # standard development 46 | npm run start:bs # as above, but with browser-sync 47 | ``` 48 | 49 | Then open a browser at [`http://localhost:3000/`](http://localhost:3000/). 50 | 51 | Some useful commands: 52 | 53 | ``` sh 54 | npm run lint # execute the eslint process on the codebase 55 | npm run karma # execute just the unit tests 56 | npm run flow-check # alias for `flow check` 57 | npm run test # execute flow checks, eslint and unit tests 58 | npm run build # run this just before you commit - the Travis-CI job runs this command 59 | ``` 60 | 61 | Most editors will report lint and type errors if properly configured. If you need help setting up 62 | your editor, we've put together a few tips for some of the more popular ones in the [Editors](./EDITORS.md) document. 63 | If your editor is not listed and you'd like to contribute some tips, feel free to make a PR :-) 64 | 65 | 66 | ### Production 67 | 68 | To test a production build: 69 | 70 | ``` sh 71 | npm run build && (cd dist && python -m SimpleHTTPServer) 72 | ``` 73 | 74 | Then open a browser at [`http://localhost:8000/`](http://localhost:8000/). 75 | 76 | ## Directory Structure 77 | ``` 78 | . 79 | ├── config # Config: mostly used by build & webpack 80 | │   ├── environments/ # Config overrides for different NODE_ENVs 81 | │   └── index.js # Config entry point. Modify to suit your needs 82 | ├── dist/ # Built artifacts get put here (e.g. webpack.output) 83 | ├── server/ # Express server files go here 84 | │   ├── index.js # Launches the express() server with webpack-hmr 85 | ├── src # The source code of the application 86 | │   ├── api/ # Modules that make API service calls 87 | │   ├── components/ # React [functional|dumb|stateless] components 88 | │   ├── containers/ # React "container" components (connected to store) 89 | │   ├── declarations/ # Flowtype declarations would go here, if necessary 90 | │   ├── pages/ # Pages: React Components that live in routes 91 | │   ├── redux/ # Here we configure our redux stores, actions, reducers... 92 | │   │   ├── modules/ # Redux modules would be collections of reducers + actions 93 | │   │   ├── configure-store.js # Redux store configured here (middleware, initial state, reducers...) 94 | │   │   └── root-reducer.js # Here we combine all our reducers 95 | │   ├── shared/ # Shared resources 96 | │   ├── static/ # Static assets. Some call this `public/` 97 | │   ├── styles/ # Global CSS styles (class names left intact) 98 | │   ├── translations/ # Our i18n translations go here 99 | │   ├── app.js # Application entry point 100 | │   ├── app-config.js # Global application settings 101 | │   ├── index.html # index.html template 102 | │   └── routes.js # All our routes 103 | ├── karma.conf.js # (self explanatory) 104 | ├── package.json # (self explanatory) 105 | ├── tests.karma.js # (self explanatory) 106 | └── webpack.config.js # (self explanatory) 107 | ``` 108 | 109 | Note: see [Dan Abramov's explanation about "Smart and Dumb Components"](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0). 110 | 111 | ## Contributing 112 | 113 | Please see [CONTRIBUTING.md](./CONTRIBUTING.md) for how to get involved. 114 | 115 | ## Changelog 116 | 117 | [Changelog](./CHANGELOG.md) 118 | 119 | ## Credits 120 | 121 | Much of the original codebase was taken from Dave Zuko's excellent [react-redux-starter-kit](https://github.com/davezuko/react-redux-starter-kit). Thanks Dave! 122 | -------------------------------------------------------------------------------- /src/styles/vendor/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | --------------------------------------------------------------------------------