├── .buckconfig ├── .editorconfig ├── .eslintrc.json ├── .flowconfig ├── .gitignore ├── .gitlab-ci.yml ├── .travis.yml ├── .watchmanconfig ├── LICENSE ├── README.md ├── android ├── app │ ├── BUCK │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── expressreacthmrboilerplate │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── keystores │ ├── BUCK │ └── debug.keystore.properties ├── configs ├── env │ ├── babel.config.dev.client.js │ ├── babel.config.dev.server.js │ ├── babel.config.prod.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── project │ ├── client.js │ ├── firebase │ │ └── client.js │ ├── mongo │ │ └── credential.tmpl.js │ ├── nodemailer │ │ └── credential.tmpl.js │ ├── passportStrategy │ │ ├── facebook │ │ │ └── credential.tmpl.js │ │ └── linkedin │ │ │ └── credential.tmpl.js │ ├── recaptcha │ │ ├── client.js │ │ └── credential.tmpl.js │ ├── server.js │ └── webpack-isomorphic-tools-configuration.js └── templates │ └── Procfile ├── gulpfile.js ├── index.android.js ├── package.json ├── specs ├── constants.js ├── endToEnd │ ├── apis │ │ ├── index.js │ │ ├── locale.js │ │ ├── todo.js │ │ └── user.js │ ├── features │ │ ├── index.js │ │ └── users.js │ ├── index.js │ └── pages.js ├── index.js ├── unit │ ├── apiEngine.js │ ├── index.js │ └── validateErrorObject.js └── utils.js └── src ├── client ├── index.js ├── setupGA.js ├── setupLocale.js └── setupNProgress.js ├── common ├── actions │ ├── apiEngine.js │ ├── cookieActions.js │ ├── entityActions.js │ ├── errorActions.js │ ├── formActions.js │ ├── intlActions.js │ ├── pageActions.js │ ├── routeActions.js │ ├── todoActions.js │ └── userActions.js ├── api │ ├── firebase.js │ ├── form.js │ ├── locale.js │ ├── todo.js │ └── user.js ├── components │ ├── fields │ │ ├── adapters │ │ │ ├── BsCheckbox.js │ │ │ ├── BsCheckboxList.js │ │ │ ├── BsInput.js │ │ │ ├── BsPlaintext.js │ │ │ ├── BsRadio.js │ │ │ ├── BsSelect.js │ │ │ ├── BsTextarea.js │ │ │ └── index.js │ │ ├── bases │ │ │ ├── AirDateRange.js │ │ │ ├── AirSingleDate.js │ │ │ ├── Checkbox.js │ │ │ ├── CheckboxListItem.js │ │ │ ├── Input.js │ │ │ ├── Plaintext.js │ │ │ ├── RadioItem.js │ │ │ ├── RangeSlider │ │ │ │ ├── RangeSlider.js │ │ │ │ └── RangeSlider.scss │ │ │ ├── Recaptcha.js │ │ │ ├── Select.js │ │ │ ├── Textarea.js │ │ │ └── index.js │ │ └── widgets │ │ │ ├── BsField.js │ │ │ ├── BsForm.js │ │ │ ├── BsFormFooter.js │ │ │ └── index.js │ ├── forms │ │ ├── DemoForm.js │ │ └── user │ │ │ ├── AvatarForm.js │ │ │ ├── ChangePasswordForm.js │ │ │ ├── EditForm.js │ │ │ ├── ForgetPasswordForm.js │ │ │ ├── LoginForm.js │ │ │ ├── RegisterForm.js │ │ │ ├── ResetPasswordForm.js │ │ │ └── VerifyEmailForm.js │ ├── layouts │ │ ├── AdminPageLayout.js │ │ ├── AppLayout.js │ │ └── PageLayout.js │ ├── pages │ │ ├── HomePage │ │ │ ├── index.js │ │ │ ├── sass-variables.scss │ │ │ ├── styles.css │ │ │ └── styles.scss │ │ ├── NotFoundPage.js │ │ ├── admin │ │ │ └── user │ │ │ │ └── ListPage.js │ │ ├── demo │ │ │ └── FormElementPage.js │ │ ├── todo │ │ │ └── ListPage.js │ │ └── user │ │ │ ├── EditPage.js │ │ │ ├── ForgetPasswordPage.js │ │ │ ├── LoginPage.js │ │ │ ├── LogoutPage.js │ │ │ ├── RegisterPage.js │ │ │ ├── ResetPasswordPage.js │ │ │ ├── ShowSelfPage.js │ │ │ └── VerifyEmailPage.js │ ├── utils │ │ ├── BsNavbar.js │ │ ├── BsPager.js │ │ ├── ErrorList.js │ │ ├── LocaleProvider.js │ │ ├── MenuItem.js │ │ ├── NavLink.js │ │ ├── Navigation.js │ │ ├── RangeSlider.js │ │ └── SocialAuthButtonList.js │ └── widgets │ │ ├── Head.js │ │ ├── Text.js │ │ └── Time.js ├── constants │ ├── ActionTypes.js │ ├── ErrorCodes.js │ ├── Errors.js │ ├── FormNames.js │ ├── Resources.js │ └── Roles.js ├── i18n │ ├── en-us.js │ └── zh-tw.js ├── reducers │ ├── apiEngineReducer.js │ ├── cookieReducer.js │ ├── entityReducer.js │ ├── errorReducer.js │ ├── formReducer.js │ ├── index.js │ ├── intlReducer.js │ ├── paginationReducer.js │ ├── routerReducer.js │ └── todoReducer.js ├── routes │ ├── admin │ │ ├── index.js │ │ └── user │ │ │ └── index.js │ ├── demo │ │ ├── formElement.js │ │ └── index.js │ ├── index.js │ ├── notFound.js │ ├── todo.js │ └── user │ │ ├── edit.js │ │ ├── forgetPassword.js │ │ ├── index.js │ │ ├── login.js │ │ ├── logout.js │ │ ├── me.js │ │ ├── register.js │ │ ├── resetPassword.js │ │ └── verifyEmail.js ├── schemas │ └── index.js └── utils │ ├── ApiEngine.js │ ├── authRequired.js │ ├── composeEnterHooks.js │ ├── deserializeCookieMap.js │ ├── ensure-polyfill.js │ ├── roleRequired.js │ └── toRefreshURL.js ├── native ├── App.js ├── Router.js ├── components │ ├── About.js │ ├── Home.js │ ├── TabIcon.js │ └── TabView.js ├── index.js ├── reducers │ ├── index.js │ └── routes.js ├── scenes.js └── styles │ └── index.scss ├── public ├── css │ └── main.css └── img │ ├── default-avatar.png │ └── logo.png └── server ├── api └── nodemailer.js ├── app.js ├── components ├── Html.js ├── ResetPasswordMail.js └── VerifyEmailMail.js ├── constants └── ErrorTypes.js ├── controllers ├── firebase.js ├── formValidation.js ├── locale.js ├── mail.js ├── react.js ├── socialAuth.js ├── ssrFetchState.js ├── todo.js └── user.js ├── decorators ├── handleError.js └── wrapTimeout.js ├── index.js ├── middlewares ├── authRequired.js ├── bodyParser.js ├── fileUpload.js ├── index.js ├── initCookie.js ├── morgan.js ├── mountHelper.js ├── mountStore.js ├── pass.js ├── passportAuth.js ├── passportInit.js ├── roleRequired.js └── validate.js ├── models ├── Todo.js ├── User.js └── plugins │ └── paginate.js ├── pm2Entry.js ├── routes ├── api.js ├── index.js ├── socialAuth.js ├── ssr.js └── ssrFetchState.js ├── server.js ├── utils ├── env.js ├── filterAttribute.js ├── getPort.js ├── tokenToURL.js └── validateErrorObject.js └── webpackIsomorphicToolsInjector.js /.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "installedESLint": true, 4 | "env": { 5 | "browser": true 6 | }, 7 | "plugins": [ 8 | "standard", 9 | "promise", 10 | "react" 11 | ], 12 | "globals": { 13 | "NProgress": true, 14 | "firebase": true, 15 | "__webpackIsomorphicTools__": true, 16 | "before": true, 17 | "after": true, 18 | "describe": true, 19 | "it": true 20 | }, 21 | "parser": "babel-eslint", 22 | "parserOptions": { 23 | "ecmaFeatures": { 24 | "jsx": true 25 | } 26 | }, 27 | "rules": { 28 | "semi": [2, "always"], 29 | "max-len": [2, { "code": 80, "ignoreComments": true, "ignoreUrls": true }], 30 | "space-before-function-paren": [2, "never"], 31 | "comma-dangle": ["error", { 32 | "arrays": "always-multiline", 33 | "objects": "always-multiline", 34 | "imports": "always-multiline", 35 | "exports": "always-multiline", 36 | "functions": "ignore" 37 | }], 38 | "handle-callback-err": 1, 39 | "no-multi-spaces": 0, 40 | "operator-linebreak": [2, "after"], 41 | "no-unused-vars": [1, { "vars": "all", "args": "none" }], 42 | "react/jsx-uses-react": "error", 43 | "react/jsx-uses-vars": "error" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*.android.js 5 | 6 | # Ignore templates with `@flow` in header 7 | .*/local-cli/generator.* 8 | 9 | # Ignore malformed json 10 | .*/node_modules/y18n/test/.*\.json 11 | 12 | # Ignore the website subdir 13 | /website/.* 14 | 15 | # Ignore BUCK generated dirs 16 | /\.buckd/ 17 | 18 | # Ignore unexpected extra @providesModule 19 | .*/node_modules/commoner/test/source/widget/share.js 20 | 21 | # Ignore duplicate module providers 22 | # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root 23 | .*/Libraries/react-native/React.js 24 | .*/Libraries/react-native/ReactNative.js 25 | .*/node_modules/jest-runtime/build/__tests__/.* 26 | 27 | [include] 28 | 29 | [libs] 30 | node_modules/react-native/Libraries/react-native/react-native-interface.js 31 | node_modules/react-native/flow 32 | flow/ 33 | 34 | [options] 35 | module.system=haste 36 | 37 | esproposal.class_static_fields=enable 38 | esproposal.class_instance_fields=enable 39 | 40 | experimental.strict_type_args=true 41 | 42 | munge_underscores=true 43 | 44 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 45 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 46 | 47 | suppress_type=$FlowIssue 48 | suppress_type=$FlowFixMe 49 | suppress_type=$FixMe 50 | 51 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(2[0-9]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 52 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(2[0-9]\\|1[0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 53 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 54 | 55 | unsafe.enable_getters_and_setters=true 56 | 57 | [version] 58 | ^0.29.0 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | 4 | # Secret 5 | *.pub 6 | *.pem 7 | *.ppk 8 | configs/**/credential.* 9 | !configs/**/credential.tmpl.* 10 | 11 | # Log 12 | *.log 13 | 14 | # Generated files 15 | /build 16 | /.deploy 17 | /src/public/tmp 18 | /src/public/users 19 | /src/native/styles/index.js 20 | my-release-key.keystore 21 | 22 | # webpack-isomorphic-tools 23 | webpack-stats.json 24 | webpack-assets.json 25 | 26 | # Android/IJ 27 | *.iml 28 | .idea 29 | *.gradle 30 | local.properties 31 | /android/build 32 | /android/app/build 33 | 34 | # iOS 35 | /ios/build 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | /android/app/libs 41 | /android/keystores/debug.keystore 42 | 43 | # Others 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: node:5.9.1 2 | 3 | cache: 4 | paths: 5 | - node_modules/ 6 | 7 | all_tests: 8 | script: 9 | - npm install 10 | - npm test 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | 8 | # ref : 9 | cache: 10 | directories: 11 | - node_modules 12 | 13 | before_script: 14 | - npm install -g mocha 15 | - npm install -g gulp 16 | 17 | before_install: 18 | - "npm install -g npm@latest" 19 | 20 | install: 21 | - npm install 22 | 23 | script: 24 | - npm test 25 | 26 | env: 27 | - CXX=g++-4.8 28 | 29 | addons: 30 | apt: 31 | sources: 32 | - ubuntu-toolchain-r-test 33 | packages: 34 | - g++-4.8 35 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 WENG CHIH-PING 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 | -------------------------------------------------------------------------------- /android/app/BUCK: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | # To learn about Buck see [Docs](https://buckbuild.com/). 4 | # To run your application with Buck: 5 | # - install Buck 6 | # - `npm start` - to start the packager 7 | # - `cd android` 8 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 9 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 10 | # - `buck install -r android/app` - compile, install and run application 11 | # 12 | 13 | lib_deps = [] 14 | for jarfile in glob(['libs/*.jar']): 15 | name = 'jars__' + re.sub(r'^.*/([^/]+)\.jar$', r'\1', jarfile) 16 | lib_deps.append(':' + name) 17 | prebuilt_jar( 18 | name = name, 19 | binary_jar = jarfile, 20 | ) 21 | 22 | for aarfile in glob(['libs/*.aar']): 23 | name = 'aars__' + re.sub(r'^.*/([^/]+)\.aar$', r'\1', aarfile) 24 | lib_deps.append(':' + name) 25 | android_prebuilt_aar( 26 | name = name, 27 | aar = aarfile, 28 | ) 29 | 30 | android_library( 31 | name = 'all-libs', 32 | exported_deps = lib_deps 33 | ) 34 | 35 | android_library( 36 | name = 'app-code', 37 | srcs = glob([ 38 | 'src/main/java/**/*.java', 39 | ]), 40 | deps = [ 41 | ':all-libs', 42 | ':build_config', 43 | ':res', 44 | ], 45 | ) 46 | 47 | android_build_config( 48 | name = 'build_config', 49 | package = 'com.expressreacthmrboilerplate', 50 | ) 51 | 52 | android_resource( 53 | name = 'res', 54 | res = 'src/main/res', 55 | package = 'com.expressreacthmrboilerplate', 56 | ) 57 | 58 | android_binary( 59 | name = 'app', 60 | package_type = 'debug', 61 | manifest = 'src/main/AndroidManifest.xml', 62 | keystore = '//android/keystores:debug', 63 | deps = [ 64 | ':app-code', 65 | ], 66 | ) 67 | -------------------------------------------------------------------------------- /android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # okhttp 54 | 55 | -keepattributes Signature 56 | -keepattributes *Annotation* 57 | -keep class okhttp3.** { *; } 58 | -keep interface okhttp3.** { *; } 59 | -dontwarn okhttp3.** 60 | 61 | # okio 62 | 63 | -keep class sun.misc.Unsafe { *; } 64 | -dontwarn java.nio.file.* 65 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 66 | -dontwarn okio.** 67 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/expressreacthmrboilerplate/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.expressreacthmrboilerplate; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "ExpressReactHmrBoilerplate"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /android/app/src/main/java/com/expressreacthmrboilerplate/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.expressreacthmrboilerplate; 2 | 3 | import android.app.Application; 4 | import android.util.Log; 5 | 6 | import com.facebook.react.ReactApplication; 7 | import com.facebook.react.ReactInstanceManager; 8 | import com.facebook.react.ReactNativeHost; 9 | import com.facebook.react.ReactPackage; 10 | import com.facebook.react.shell.MainReactPackage; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 18 | @Override 19 | protected boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | return Arrays.asList( 26 | new MainReactPackage() 27 | ); 28 | } 29 | }; 30 | 31 | @Override 32 | public ReactNativeHost getReactNativeHost() { 33 | return mReactNativeHost; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExpressReactHmrBoilerplate 3 | 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip 6 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = 'debug', 3 | store = 'debug.keystore', 4 | properties = 'debug.keystore.properties', 5 | visibility = [ 6 | 'PUBLIC', 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /configs/env/babel.config.dev.client.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'transform-decorators-legacy', 4 | 'transform-class-properties', 5 | ], 6 | presets: [ 7 | 'react-hmre', 8 | 'stage-0', 9 | 'es2015', 10 | 'react', 11 | ], 12 | cacheDirectory: true, 13 | }; 14 | -------------------------------------------------------------------------------- /configs/env/babel.config.dev.server.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'transform-decorators-legacy', 4 | 'transform-class-properties', 5 | ], 6 | presets: [ 7 | 'stage-0', 8 | 'es2015', 9 | 'react', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /configs/env/babel.config.prod.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'transform-decorators-legacy', 4 | 'transform-class-properties', 5 | ], 6 | presets: [ 7 | 'stage-0', 8 | 'es2015', 9 | 'react', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /configs/env/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 4 | var webpackIsomorphicToolsConfig = require('../project/webpack-isomorphic-tools-configuration'); 5 | var babelConfig = require('./babel.config.dev.client'); 6 | 7 | var webpackIsomorphicToolsPlugin = 8 | new WebpackIsomorphicToolsPlugin(webpackIsomorphicToolsConfig) 9 | .development(); 10 | 11 | module.exports = { 12 | // project root, sync with `./webpack.config.prod.js` and `src/webpackIsomorphicToolsInjector.js` 13 | context: path.resolve(__dirname, '../../src'), 14 | devtool: 'cheap-module-eval-source-map', 15 | entry: [ 16 | 'eventsource-polyfill', 17 | 'webpack-hot-middleware/client', 18 | path.join(__dirname, '../../src/client/index'), 19 | ], 20 | output: { 21 | path: path.join(__dirname, '../../build/public/js'), 22 | filename: 'bundle.js', 23 | chunkFilename: '[id].[hash].chunk.js', 24 | publicPath: '/js/', 25 | }, 26 | externals: { 27 | jquery: 'jQuery', 28 | mongoose: 'mongoose', 29 | }, 30 | plugins: [ 31 | new webpack.HotModuleReplacementPlugin(), 32 | new webpack.DefinePlugin({ 33 | 'process.env': { 34 | NODE_ENV: JSON.stringify('development'), 35 | BROWSER: JSON.stringify(true), 36 | }, 37 | }), 38 | new webpack.NoErrorsPlugin(), 39 | webpackIsomorphicToolsPlugin, 40 | ], 41 | module: { 42 | loaders: [{ 43 | test: /\.jsx?$/, 44 | include: path.join(__dirname, '../../src'), 45 | loader: 'babel', 46 | query: babelConfig, 47 | }, { 48 | test: webpackIsomorphicToolsPlugin.regular_expression('cssModules'), 49 | loaders: [ 50 | 'style-loader', 51 | 'css-loader', 52 | 'postcss-loader', 53 | 'sass-loader', 54 | ], 55 | }], 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /configs/env/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin'); 4 | var webpackIsomorphicToolsConfig = require('../project/webpack-isomorphic-tools-configuration'); 5 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 6 | var babelConfig = require('./babel.config.prod'); 7 | 8 | var webpackIsomorphicToolsPlugin = 9 | new WebpackIsomorphicToolsPlugin(webpackIsomorphicToolsConfig); 10 | 11 | module.exports = { 12 | // project root, sync with `./webpack.config.dev.js` and `src/webpackIsomorphicToolsInjector.js` 13 | context: path.resolve(__dirname, '../../src'), 14 | // devtool: 'source-map', 15 | entry: [ 16 | path.join(__dirname, '../../src/client/index'), 17 | ], 18 | output: { 19 | path: path.join(__dirname, '../../build/public/js'), 20 | filename: 'bundle.js', 21 | chunkFilename: '[id].[hash].chunk.js', 22 | publicPath: '/js/', 23 | }, 24 | externals: { 25 | jquery: 'jQuery', 26 | mongoose: 'mongoose', 27 | }, 28 | plugins: [ 29 | new webpack.optimize.OccurenceOrderPlugin(), 30 | new webpack.DefinePlugin({ 31 | 'process.env': { 32 | NODE_ENV: JSON.stringify('production'), 33 | BROWSER: JSON.stringify(true), 34 | }, 35 | }), 36 | new webpack.optimize.MinChunkSizePlugin({ 37 | minChunkSize: 10000, 38 | }), 39 | new webpack.optimize.AggressiveMergingPlugin(), 40 | new webpack.optimize.UglifyJsPlugin({ 41 | compress: { 42 | dead_code: true, 43 | warnings: false, 44 | }, 45 | comments: false, 46 | sourceMap: false, 47 | }), 48 | new ExtractTextPlugin('[name].[contenthash].css', { 49 | allChunks: true, 50 | }), 51 | webpackIsomorphicToolsPlugin, 52 | ], 53 | module: { 54 | loaders: [{ 55 | test: /\.jsx?$/, 56 | include: path.join(__dirname, '../../src'), 57 | loader: 'babel', 58 | query: babelConfig, 59 | }, { 60 | test: webpackIsomorphicToolsPlugin.regular_expression('cssModules'), 61 | loader: ExtractTextPlugin.extract('style', [ 62 | 'css-loader', 63 | 'postcss-loader', 64 | 'sass-loader', 65 | ]), 66 | }], 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /configs/project/client.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | firebase: require('./firebase/client'), 3 | GA: { 4 | development: { 5 | trackingID: 'UA-86112397-2', 6 | }, 7 | production: { 8 | trackingID: 'UA-86112397-1', 9 | }, 10 | }, 11 | recaptcha: require('./recaptcha/client'), 12 | fileUpload: { 13 | avatar: { 14 | maxSize: 1024 * 1024, // in bytes 15 | // MIME type 16 | validMIMETypes: [ 17 | 'image/jpeg', 18 | 'image/png', 19 | 'image/gif', 20 | ], 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /configs/project/firebase/client.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | apiKey: "AIzaSyARRM3F_eEaKVKvupqzD181KI7D_q7ASO0", 3 | authDomain: "express-react-hmr-boilerplate.firebaseapp.com", 4 | databaseURL: "https://express-react-hmr-boilerplate.firebaseio.com", 5 | storageBucket: "express-react-hmr-boilerplate.appspot.com", 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /configs/project/mongo/credential.tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: 'mongodb://:@:/-development', 3 | test: 'mongodb://:@:/-test', 4 | production: 'mongodb://:@:/-production', 5 | }; 6 | -------------------------------------------------------------------------------- /configs/project/nodemailer/credential.tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // ref: 3 | // - 4 | // - 5 | development: { 6 | service: 'gmail', 7 | auth: { 8 | user: 'your_gmail_username', 9 | pass: 'your_gmail_password', 10 | }, 11 | }, 12 | test: { 13 | service: 'gmail', 14 | auth: { 15 | user: 'your_gmail_username', 16 | pass: 'your_gmail_password', 17 | }, 18 | }, 19 | production: { 20 | service: 'gmail', 21 | auth: { 22 | user: 'your_gmail_username', 23 | pass: 'your_gmail_password', 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /configs/project/passportStrategy/facebook/credential.tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: { 3 | // ref: 4 | profileFields: ['id', 'displayName', 'email', 'picture'], 5 | }, 6 | development: { 7 | clientID: '121212121212121', 8 | clientSecret: 'abcedfghi123456789abcdefghi12345', 9 | callbackURL: 'http://localhost:3000/auth/facebook/callback', 10 | }, 11 | test: { 12 | clientID: '121212121212121', 13 | clientSecret: 'abcedfghi123456789abcdefghi12345', 14 | callbackURL: 'http://localhost:5566/auth/facebook/callback', 15 | }, 16 | production: { 17 | clientID: '343434343434343', 18 | clientSecret: 'abcedfghi123456789abcdefghi12345', 19 | callbackURL: 'https://express-react-hmr-boilerplate.herokuapp.com/auth/facebook/callback', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /configs/project/passportStrategy/linkedin/credential.tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | default: { 3 | scope: ['r_basicprofile', 'r_emailaddress'], 4 | passReqToCallback: true, 5 | }, 6 | development: { 7 | clientID: '7a7a7a7a7a7a7a', 8 | clientSecret: 'B9B9B9B9B9B9B9B9', 9 | callbackURL: 'http://localhost:3000/auth/linkedin/callback', 10 | }, 11 | test: { 12 | clientID: '7a7a7a7a7a7a7a', 13 | clientSecret: 'B9B9B9B9B9B9B9B9', 14 | callbackURL: 'http://localhost:5566/auth/linkedin/callback', 15 | }, 16 | production: { 17 | clientID: '7a7a7a7a7a7a7a', 18 | clientSecret: 'B9B9B9B9B9B9B9B9', 19 | callbackURL: 'https://express-react-hmr-boilerplate.herokuapp.com/auth/linkedin/callback', 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /configs/project/recaptcha/client.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | siteKey: '6LfQGgoUAAAAAANHp6AvQOYD81JU9GnThpiIK7pH', 4 | }, 5 | test: { 6 | siteKey: '6LfQGgoUAAAAAANHp6AvQOYD81JU9GnThpiIK7pH', 7 | }, 8 | production: { 9 | siteKey: '6LeoHAoUAAAAAHKlo43OuPREJb22GLmik2HSaFC1', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /configs/project/recaptcha/credential.tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | development: { 3 | secretKey: 'akb48akb48akb48akb48akb48akb48akb48akb48', 4 | }, 5 | test: { 6 | secretKey: 'akb48akb48akb48akb48akb48akb48akb48akb48', 7 | }, 8 | production: { 9 | secretKey: 'akc49akc49akc49akc49akc49akc49akc49akc49', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /configs/project/server.js: -------------------------------------------------------------------------------- 1 | if (process.env.TRAVIS) { 2 | if (!process.env.PROJECT_SERVER_CONFIGS) { 3 | throw new Error( 4 | 'Environment variable `PROJECT_SERVER_CONFIGS` is not set. ' + 5 | 'Please dump it by running `gulp dumpConfigs`' 6 | ); 7 | } 8 | module.exports = JSON.parse(process.env.PROJECT_SERVER_CONFIGS); 9 | } else { 10 | module.exports = { 11 | host: { 12 | development: 'http://localhost:3000', 13 | test: 'http://localhost:5566', 14 | production: 'https://express-react-hmr-boilerplate.herokuapp.com', 15 | }, 16 | jwt: { 17 | authentication: { 18 | secret: '4eO5viHe23', 19 | expiresIn: 60 * 60 * 24 * 3, // in seconds 20 | }, 21 | verifyEmail: { 22 | secret: 'df5s6sdHdjJdRg56', 23 | expiresIn: 60 * 60, // in seconds 24 | }, 25 | resetPassword: { 26 | secret: 'FsgWqLhX0Z6JvJfPYwPZ', 27 | expiresIn: 60 * 60, // in seconds 28 | }, 29 | }, 30 | mongo: require('./mongo/credential'), 31 | firebase: require('./firebase/credential.json'), 32 | passportStrategy: { 33 | facebook: require('./passportStrategy/facebook/credential'), 34 | linkedin: require('./passportStrategy/linkedin/credential'), 35 | }, 36 | recaptcha: require('./recaptcha/credential'), 37 | nodemailer: require('./nodemailer/credential'), 38 | mailOptions: { 39 | default: { 40 | subject: 'Untitled Mail', 41 | from: 'Express-React-Hmr-Boilerplate ', 42 | text: 'No Text', 43 | html: '
no html content
',
44 |       },
45 |       development: {
46 |         to: 'gocreating@gmail.com',
47 |       },
48 |       test: {
49 |         to: 'dont_sent_to_me_when_every_test@gmail.com',
50 |       },
51 |     },
52 |   };
53 | }
54 | 


--------------------------------------------------------------------------------
/configs/project/webpack-isomorphic-tools-configuration.js:
--------------------------------------------------------------------------------
 1 | var WebpackIsomorphicToolsPlugin = require('webpack-isomorphic-tools/plugin');
 2 | 
 3 | module.exports = {
 4 |   // debug: true,
 5 |   // relative from webpackConfig.context
 6 |   webpack_assets_file_path: '../configs/project/isomorphic/webpack-assets.json',
 7 |   webpack_stats_file_path: '../configs/project/isomorphic/webpack-stats.json',
 8 |   assets: {
 9 |     cssModules: {
10 |       extensions: ['css', 'scss'],
11 |       filter: function(module, regex, options, log) {
12 |         if (options.development) {
13 |           // in development mode there's webpack "style-loader",
14 |           // so the module.name is not equal to module.name
15 |           return WebpackIsomorphicToolsPlugin.style_loader_filter(module, regex, options, log);
16 |         } else {
17 |           // in production mode there's no webpack "style-loader",
18 |           // so the module.name will be equal to the asset path
19 |           return regex.test(module.name);
20 |         }
21 |       },
22 |       path: function(module, options, log) {
23 |         if (options.development) {
24 |           // in development mode there's webpack "style-loader",
25 |           // so the module.name is not equal to module.name
26 |           return WebpackIsomorphicToolsPlugin.style_loader_path_extractor(module, options, log);
27 |         } else {
28 |           // in production mode there's no webpack "style-loader",
29 |           // so the module.name will be equal to the asset path
30 |           return module.name;
31 |         }
32 |       },
33 |       parser: function(module, options, log) {
34 |         if (options.development) {
35 |           return WebpackIsomorphicToolsPlugin.css_modules_loader_parser(module, options, log);
36 |         } else {
37 |           // in production mode there's Extract Text Loader which extracts CSS text away
38 |           return module.source;
39 |         }
40 |       }
41 |     },
42 |   },
43 | };
44 | 


--------------------------------------------------------------------------------
/configs/templates/Procfile:
--------------------------------------------------------------------------------
1 | web: npm run pm2
2 | 


--------------------------------------------------------------------------------
/index.android.js:
--------------------------------------------------------------------------------
1 | import './src/native/index';
2 | 


--------------------------------------------------------------------------------
/specs/constants.js:
--------------------------------------------------------------------------------
1 | var PORT = 5566;
2 | var BASE = 'http://localhost:' + PORT;
3 | 
4 | module.exports = {
5 |   NODE_ENV: 'test',
6 |   PORT: PORT,
7 |   BASE: BASE,
8 | };
9 | 


--------------------------------------------------------------------------------
/specs/endToEnd/apis/index.js:
--------------------------------------------------------------------------------
1 | describe('#APIs', () => {
2 |   require('./user');
3 |   require('./locale');
4 |   require('./todo');
5 | });
6 | 


--------------------------------------------------------------------------------
/specs/endToEnd/apis/locale.js:
--------------------------------------------------------------------------------
 1 | import chai from 'chai';
 2 | import { apiEngine } from '../../utils';
 3 | import localeAPI from '../../../build/common/api/locale';
 4 | import async from 'async';
 5 | import Errors from '../../../build/common/constants/Errors';
 6 | let expect = chai.expect;
 7 | 
 8 | describe('#localeAPI', () => {
 9 |   let validLocales = [
10 |     'en-us',
11 |     'zh-tw',
12 |   ];
13 | 
14 |   let invalidLocales = [
15 |     'foo',
16 |     'bar',
17 |     'fuck you',
18 |   ];
19 | 
20 |   describe('#read()', () => {
21 |     it('should download valid locale', (done) => {
22 |       async.eachSeries(validLocales,  (validLocale, cb) => {
23 |         localeAPI(apiEngine)
24 |           .read(validLocale)
25 |           .then((json) => {
26 |             expect(json.locale).to.equal(validLocale);
27 |             expect(json.messages).to.be.an('object');
28 |             cb();
29 |           });
30 |       }, done);
31 |     });
32 | 
33 |     it('should reject invalid locale', (done) => {
34 |       async.eachSeries(invalidLocales, (invalidLocale, cb) => {
35 |         localeAPI(apiEngine)
36 |           .read(invalidLocale)
37 |           .catch((err) => {
38 |             expect(err[0].code)
39 |               .to.equal(Errors.LOCALE_NOT_SUPPORTED.code);
40 |             cb();
41 |           });
42 |       }, done);
43 |     });
44 |   });
45 | });
46 | 


--------------------------------------------------------------------------------
/specs/endToEnd/apis/todo.js:
--------------------------------------------------------------------------------
 1 | import chai from 'chai';
 2 | import { apiEngine } from '../../utils';
 3 | import todoAPI from '../../../build/common/api/todo';
 4 | import async from 'async';
 5 | import Todo from '../../../build/server/models/Todo';
 6 | let expect = chai.expect;
 7 | 
 8 | describe('#todoAPI', () => {
 9 |   let fakeTodos = [{
10 |     text: 'this is a fake todo text',
11 |   }, {
12 |     text: 'foo',
13 |   }, {
14 |     text: '~bar~',
15 |   }];
16 | 
17 |   before((done) => {
18 |     Todo.remove({}, done);
19 |   });
20 | 
21 |   describe('#create()', () => {
22 |     it('should create todo', (done) => {
23 |       async.eachSeries(fakeTodos, (fakeTodo, cb) => {
24 |         todoAPI(apiEngine)
25 |           .create(fakeTodo)
26 |           .then((json) => {
27 |             expect(json.todo).to.be.an('object');
28 |             expect(json.todo.text).to.equal(fakeTodo.text);
29 |             cb();
30 |           });
31 |       }, done);
32 |     });
33 |   });
34 | 
35 |   describe('#list()', () => {
36 |     it('should list todos', (done) => {
37 |       todoAPI(apiEngine)
38 |         .list({ page: 1 })
39 |         .then((json) => {
40 |           expect(json.todos).to.be.an('array');
41 |           expect(json.todos).to.have.lengthOf(fakeTodos.length);
42 |           done();
43 |         });
44 |     });
45 |   });
46 | 
47 |   after((done) => {
48 |     Todo.remove({}, done);
49 |   });
50 | });
51 | 


--------------------------------------------------------------------------------
/specs/endToEnd/features/index.js:
--------------------------------------------------------------------------------
1 | import users from './users';
2 | 
3 | export default {
4 |   users,
5 | };
6 | 


--------------------------------------------------------------------------------
/specs/endToEnd/features/users.js:
--------------------------------------------------------------------------------
 1 | import Roles from '../../../build/common/constants/Roles';
 2 | 
 3 | export default {
 4 |   users: [{
 5 |     name: 'test1',
 6 |     email: {
 7 |       value: 'fakeUser1@gmail.com',
 8 |     },
 9 |     password: 'fake',
10 |     role: Roles.USER,
11 |   }, {
12 |     name: 'test2',
13 |     email: {
14 |       value: 'fakeUser2@gmail.com',
15 |     },
16 |     password: 'fake123',
17 |     role: Roles.USER,
18 |   }],
19 |   admins: [{
20 |     name: 'admin',
21 |     email: {
22 |       value: 'admin@gmail.com',
23 |     },
24 |     password: 'admin',
25 |     role: Roles.ADMIN,
26 |   }],
27 | };
28 | 


--------------------------------------------------------------------------------
/specs/endToEnd/index.js:
--------------------------------------------------------------------------------
 1 | import constants from '../constants';
 2 | import appPromise from '../../build/server/app';
 3 | let server;
 4 | 
 5 | before((done) => {
 6 |   appPromise.then((app) => {
 7 |     console.log('\nstarting server on port', constants.PORT, '...\n');
 8 |     server = app.listen(constants.PORT, done);
 9 |   });
10 | });
11 | 
12 | require('./pages');
13 | require('./apis');
14 | 
15 | after((done) => {
16 |   console.log('\nclosing server...\n');
17 |   if (server) {
18 |     server.close();
19 |   }
20 |   done();
21 | });
22 | 


--------------------------------------------------------------------------------
/specs/endToEnd/pages.js:
--------------------------------------------------------------------------------
  1 | import chai from 'chai';
  2 | import { clearUsers, prepareUsers } from '../utils';
  3 | import request from 'superagent';
  4 | import constants from '../constants';
  5 | let expect = chai.expect;
  6 | 
  7 | describe('#Pages', () => {
  8 |   let reqs = {
  9 |     users: [],
 10 |     admins: [],
 11 |   };
 12 |   let userInstances = {
 13 |     users: [],
 14 |     admins: [],
 15 |   };
 16 |   let pages = {
 17 |     public: [
 18 |       '/',
 19 |       '/user/register',
 20 |       '/user/login',
 21 |       '/user/email/verify',
 22 |       '/user/password/forget',
 23 |       '/user/password/reset',
 24 |     ],
 25 |     normalUserRequired: [
 26 |       '/user/me',
 27 |       '/user/me/edit',
 28 |     ],
 29 |     adminUserRequired: [
 30 |       '/admin/user',
 31 |     ],
 32 |   };
 33 | 
 34 |   before((done) => {
 35 |     clearUsers(() => prepareUsers(reqs, userInstances, done));
 36 |   });
 37 | 
 38 |   describe('#Unauth User', () => {
 39 |     pages.public.forEach((page) => {
 40 |       describe(`GET ${page}`, () => {
 41 |         it('should access a public page', (cb) => {
 42 |           request
 43 |             .get(constants.BASE + page)
 44 |             .end((err, res) => {
 45 |               expect(err).to.equal(null);
 46 |               expect(res).to.not.be.undefined;
 47 |               expect(res.status).to.equal(200);
 48 |               cb();
 49 |             });
 50 |         });
 51 |       });
 52 |     });
 53 | 
 54 |     pages.normalUserRequired.forEach((page) => {
 55 |       describe(`GET ${page}`, () => {
 56 |         it('should be redirected to login page', (cb) => {
 57 |           request
 58 |             .get(constants.BASE + page)
 59 |             .end((err, res) => {
 60 |               expect(err).to.equal(null);
 61 |               expect(res).to.not.be.undefined;
 62 |               expect(res.status).to.equal(200);
 63 |               expect(res.redirects).to.have.length.above(0);
 64 |               expect(res.redirects[0]).to.have.string('/user/login');
 65 |               cb();
 66 |             });
 67 |         });
 68 |       });
 69 |     });
 70 |   });
 71 | 
 72 |   describe('#Normal User', () => {
 73 |     (pages.public.concat(pages.normalUserRequired)).forEach((page) => {
 74 |       describe(`GET ${page}`, () => {
 75 |         it('should access both public and normal-user-only pages', (cb) => {
 76 |           request
 77 |             .get(constants.BASE + page)
 78 |             .set('Cookie', reqs.users[0].get('cookie'))
 79 |             .end((err, res) => {
 80 |               expect(err).to.equal(null);
 81 |               expect(res).to.not.be.undefined;
 82 |               expect(res.status).to.equal(200);
 83 |               cb();
 84 |             });
 85 |         });
 86 |       });
 87 |     });
 88 |   });
 89 | 
 90 |   describe('#Admin User', () => {
 91 |     (pages.public.concat(pages.adminUserRequired)).forEach((page) => {
 92 |       describe(`GET ${page}`, () => {
 93 |         it('should access both public and admin-only pages', (cb) => {
 94 |           request
 95 |             .get(constants.BASE + page)
 96 |             .set('Cookie', reqs.users[0].get('cookie'))
 97 |             .end((err, res) => {
 98 |               expect(err).to.equal(null);
 99 |               expect(res).to.not.be.undefined;
100 |               expect(res.status).to.equal(200);
101 |               cb();
102 |             });
103 |         });
104 |       });
105 |     });
106 |   });
107 | 
108 |   after((done) => {
109 |     clearUsers(done);
110 |   });
111 | });
112 | 


--------------------------------------------------------------------------------
/specs/index.js:
--------------------------------------------------------------------------------
 1 | require('babel-register')(require('../configs/env/babel.config.dev.server'));
 2 | var constants = require('./constants');
 3 | process.env.NODE_ENV = constants.NODE_ENV;
 4 | process.env.PORT = constants.PORT;
 5 | 
 6 | describe('#Unit', function() {
 7 |   require('./unit');
 8 | });
 9 | 
10 | describe('#EndToEnd', function() {
11 |   require('./endToEnd');
12 | });
13 | 


--------------------------------------------------------------------------------
/specs/unit/apiEngine.js:
--------------------------------------------------------------------------------
 1 | import chai from 'chai';
 2 | import ApiEngine from '../../build/common/utils/ApiEngine';
 3 | let expect = chai.expect;
 4 | 
 5 | describe('#ApiEngine', () => {
 6 |   let apiEngine = new ApiEngine();
 7 |   it('should provide http verb methods', () => {
 8 |     expect(apiEngine.get).to.be.a('function');
 9 |     expect(apiEngine.post).to.be.a('function');
10 |     expect(apiEngine.put).to.be.a('function');
11 |     expect(apiEngine.patch).to.be.a('function');
12 |     expect(apiEngine.del).to.be.a('function');
13 |   });
14 | });
15 | 


--------------------------------------------------------------------------------
/specs/unit/index.js:
--------------------------------------------------------------------------------
1 | require('./apiEngine');
2 | require('./validateErrorObject');
3 | 


--------------------------------------------------------------------------------
/specs/unit/validateErrorObject.js:
--------------------------------------------------------------------------------
 1 | import chai from 'chai';
 2 | import validateErrorObject from '../../build/server/utils/validateErrorObject';
 3 | let expect = chai.expect;
 4 | 
 5 | describe('#validateErrorObject', () => {
 6 |   it('should pass when there is no error', () => {
 7 |     expect(validateErrorObject({})).to.be.true;
 8 |     expect(validateErrorObject({
 9 |       nested: {},
10 |     })).to.be.true;
11 |     expect(validateErrorObject({
12 |       multiple: {},
13 |       nested: {},
14 |       fields: {},
15 |     })).to.be.true;
16 |     expect(validateErrorObject({
17 |       nested: {
18 |         foo: {},
19 |         bar: {},
20 |       },
21 |     })).to.be.true;
22 |   });
23 |   it('should reject when there is error', () => {
24 |     expect(validateErrorObject({
25 |       field1: 'some error happened to field1',
26 |     })).to.be.false;
27 |     expect(validateErrorObject({
28 |       field1: 'some error happened to field1',
29 |       field2: 'some error happened to field2',
30 |     })).to.be.false;
31 |     expect(validateErrorObject({
32 |       nested: {
33 |         field1: 'some error happened to nested field1',
34 |       },
35 |     })).to.be.false;
36 |     expect(validateErrorObject({
37 |       multiple: {
38 |         nested1: 'some error happened to nested field1',
39 |         nested2: 'some error happened to nested field2',
40 |       },
41 |     })).to.be.false;
42 |     expect(validateErrorObject({
43 |       deep: {
44 |         nested: {
45 |           field1: 'some error happened to deep nested field1',
46 |         },
47 |       },
48 |     })).to.be.false;
49 |   });
50 | });
51 | 


--------------------------------------------------------------------------------
/specs/utils.js:
--------------------------------------------------------------------------------
 1 | import ApiEngine from '../build/common/utils/ApiEngine';
 2 | import User from '../build/server/models/User';
 3 | import features from './endToEnd/features';
 4 | 
 5 | export let serializeCookie = (cookieObject) => (
 6 |   Object
 7 |     .keys(cookieObject)
 8 |     .map((key) => `${key}=${cookieObject[key]}`)
 9 |     .join(';')
10 | );
11 | 
12 | export let getReq = ({ cookie }) => ({
13 |   get: (key) => {
14 |     if (key === 'cookie') {
15 |       return serializeCookie(cookie);
16 |     }
17 |   },
18 | });
19 | 
20 | export let clearUsers = (cb) => {
21 |   User.remove({}, cb);
22 | };
23 | 
24 | export let prepareUsers = (reqs, userInstances, cb) => {
25 |   reqs.users = [];
26 |   reqs.admins = [];
27 |   userInstances.users = [];
28 |   userInstances.admins = [];
29 | 
30 |   User(features.users.users[0]).save((err, user) => {
31 |     userInstances.users.push(user);
32 |     User(features.users.admins[0]).save((err, user) => {
33 |       userInstances.admins.push(user);
34 | 
35 |       userInstances.users.forEach((normalUser) => {
36 |         reqs.users.push(getReq({ cookie: {
37 |           token: normalUser.toAuthenticationToken(),
38 |         }}));
39 |       });
40 |       userInstances.admins.forEach((adminUser) => {
41 |         reqs.admins.push(getReq({ cookie: {
42 |           token: adminUser.toAuthenticationToken(),
43 |         }}));
44 |       });
45 |       cb(err);
46 |     });
47 |   });
48 | };
49 | 
50 | export let apiEngine = new ApiEngine(getReq({
51 |   cookie: {},
52 | }));
53 | 


--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import { render } from 'react-dom';
 3 | import { createStore, applyMiddleware } from 'redux';
 4 | import { Provider } from 'react-redux';
 5 | import thunk from 'redux-thunk';
 6 | import { match, Router, browserHistory } from 'react-router';
 7 | import {
 8 |   routerMiddleware,
 9 |   syncHistoryWithStore,
10 |   push,
11 | } from 'react-router-redux';
12 | import LocaleProvider from '../common/components/utils/LocaleProvider';
13 | import rootReducer from '../common/reducers';
14 | import getRoutes from '../common/routes';
15 | import setupLocale from './setupLocale';
16 | import setupNProgress from './setupNProgress';
17 | import setupGA from './setupGA';
18 | import { setApiEngine } from '../common/actions/apiEngine';
19 | import { removeCookie } from '../common/actions/cookieActions';
20 | import ApiEngine from '../common/utils/ApiEngine';
21 | 
22 | setupNProgress();
23 | setupLocale();
24 | let logPageView = setupGA();
25 | const initialState = window.__INITIAL_STATE__;
26 | let store = createStore(
27 |   rootReducer,
28 |   initialState,
29 |   applyMiddleware(
30 |     routerMiddleware(browserHistory),
31 |     thunk
32 |   )
33 | );
34 | 
35 | let apiEngine = new ApiEngine();
36 | store.dispatch(setApiEngine(apiEngine));
37 | 
38 | let { redirect } = store.getState().cookies;
39 | if (redirect) {
40 |   store.dispatch(push(redirect));
41 |   store.dispatch(removeCookie('redirect'));
42 | }
43 | 
44 | // refs:
45 | // - 
46 | // - 
47 | let history = syncHistoryWithStore(browserHistory, store);
48 | let routes = getRoutes(store);
49 | match({
50 |   history,
51 |   routes,
52 | }, (error, redirectLocation, renderProps) => {
53 |   if (error) {
54 |     console.log(error);
55 |   }
56 |   render(
57 |     
58 |       
59 |         
64 |           {routes}
65 |         
66 |       
67 |     
68 |   , document.getElementById('root'));
69 | });
70 | 


--------------------------------------------------------------------------------
/src/client/setupGA.js:
--------------------------------------------------------------------------------
 1 | import reactGA from 'react-ga';
 2 | import configs from '../../configs/project/client';
 3 | 
 4 | export default () => {
 5 |   if (configs.GA) {
 6 |     reactGA.initialize(configs.GA[process.env.NODE_ENV].trackingID);
 7 | 
 8 |     return function logPageView() {
 9 |       reactGA.set({ page: window.location.pathname });
10 |       reactGA.pageview(window.location.pathname);
11 |     };
12 |   }
13 |   return undefined;
14 | };
15 | 


--------------------------------------------------------------------------------
/src/client/setupLocale.js:
--------------------------------------------------------------------------------
 1 | import { addLocaleData } from 'react-intl';
 2 | import en from 'react-intl/locale-data/en';
 3 | import zh from 'react-intl/locale-data/zh';
 4 | 
 5 | export default () => {
 6 |   addLocaleData(en);
 7 |   addLocaleData(zh);
 8 |   addLocaleData({
 9 |     locale: 'en-us',
10 |     parentLocale: 'en',
11 |   });
12 |   addLocaleData({
13 |     locale: 'zh-tw',
14 |     parentLocale: 'zh',
15 |   });
16 | };
17 | 


--------------------------------------------------------------------------------
/src/client/setupNProgress.js:
--------------------------------------------------------------------------------
 1 | // refs:
 2 | // - 
 3 | // - 
 4 | export default () => {
 5 |   class ProgressBar {
 6 |     constructor() {
 7 |       this.ajaxRequestCount = 0;
 8 |     }
 9 |     start() {
10 |       this.ajaxRequestCount++;
11 |       if (this.ajaxRequestCount === 1) {
12 |         NProgress.start();
13 |       }
14 |     }
15 |     done() {
16 |       this.ajaxRequestCount--;
17 |       if (this.ajaxRequestCount < 0) {
18 |         this.ajaxRequestCount = 0;
19 |       }
20 |       if (this.ajaxRequestCount === 0) {
21 |         NProgress.done();
22 |       }
23 |     }
24 |   }
25 |   let progressBar = new ProgressBar();
26 |   let oldOpen = XMLHttpRequest.prototype.open;
27 | 
28 |   function onStateChange() {
29 |     if (this.readyState === 1) {
30 |       progressBar.start();
31 |     } else if (this.readyState === 4) {
32 |       progressBar.done();
33 |     } else {
34 |       // console.log('[xhr waiting]');
35 |     }
36 |   }
37 | 
38 |   XMLHttpRequest.prototype.open = function() {
39 |     this.addEventListener('readystatechange', onStateChange);
40 |     oldOpen.apply(this, arguments);
41 |   };
42 | 
43 |   let observeDOM = (function() {
44 |     let MutationObserver = window.MutationObserver ||
45 |                            window.WebKitMutationObserver;
46 |     let eventListenerSupported = window.addEventListener;
47 | 
48 |     return function(obj, onStart, onDone) {
49 |       let caller = function(ele) {
50 |         if (ele.nodeName === 'SCRIPT') {
51 |           onStart();
52 |           ele.onload = function() {
53 |             ele.onload = null;
54 |             onDone();
55 |           };
56 |         }
57 |       };
58 |       if (MutationObserver) {
59 |         let obs = new MutationObserver(function(mutations, observer) {
60 |           let ele = mutations[0].addedNodes.length &&
61 |                     mutations[0].addedNodes[0];
62 |           caller(ele);
63 |         });
64 |         obs.observe(obj, { childList: true, subtree: true });
65 |       } else if (eventListenerSupported) {
66 |         obj.addEventListener('DOMNodeInserted', function(e) {
67 |           caller(e.srcElement);
68 |         }, false);
69 |       }
70 |     };
71 |   })();
72 | 
73 |   observeDOM(document.head,
74 |     function onStart() {
75 |       progressBar.start();
76 |     },
77 |     function onDone() {
78 |       progressBar.done();
79 |     }
80 |   );
81 | };
82 | 


--------------------------------------------------------------------------------
/src/common/actions/apiEngine.js:
--------------------------------------------------------------------------------
1 | import ActionTypes from '../constants/ActionTypes';
2 | 
3 | export const setApiEngine = (apiEngine) => {
4 |   return {
5 |     type: ActionTypes.SET_API_ENGINE,
6 |     apiEngine,
7 |   };
8 | };
9 | 


--------------------------------------------------------------------------------
/src/common/actions/cookieActions.js:
--------------------------------------------------------------------------------
 1 | import cookie from 'cookie';
 2 | import assign from 'object-assign';
 3 | import isString from 'lodash/isString';
 4 | import isObject from 'lodash/isObject';
 5 | import ActionTypes from '../constants/ActionTypes';
 6 | import { deserializeCookie } from '../utils/deserializeCookieMap';
 7 | 
 8 | export const setCookie = (name, value, options, res = null) => {
 9 |   options = assign({
10 |     path: '/',
11 |   }, options);
12 |   let deserializedValue = deserializeCookie(value);
13 | 
14 |   return (dispatch, getState) => {
15 |     let serializedValue;
16 | 
17 |     if (isString(value)) {
18 |       serializedValue = value;
19 |     } else if (isObject(value)) {
20 |       serializedValue = JSON.stringify(value);
21 |     }
22 | 
23 |     if (process.env.BROWSER) {
24 |       document.cookie = cookie.serialize(
25 |         name, serializedValue, options);
26 |     } else if (res) {
27 |       res.cookie(name, serializedValue);
28 |     }
29 | 
30 |     dispatch({
31 |       type: ActionTypes.SET_COOKIE,
32 |       cookie: {
33 |         name,
34 |         value: deserializedValue,
35 |         options,
36 |       },
37 |     });
38 |   };
39 | };
40 | 
41 | export const setCookies = (cookies, res = null) => {
42 |   return (dispatch) => {
43 |     Object
44 |       .keys(cookies)
45 |       .forEach((name) => dispatch(setCookie(name, cookies[name], {}, res)));
46 |   };
47 | };
48 | 
49 | export const removeCookie = (name) => {
50 |   return (dispatch, getState) => {
51 |     if (process.env.BROWSER) {
52 |       return dispatch(setCookie(name, '', {
53 |         expires: new Date(1970, 1, 1, 0, 0, 1),
54 |       }));
55 |     }
56 |     return Promise.resolve();
57 |   };
58 | };
59 | 


--------------------------------------------------------------------------------
/src/common/actions/entityActions.js:
--------------------------------------------------------------------------------
 1 | import ActionTypes from '../constants/ActionTypes';
 2 | 
 3 | export const setEntities = (normalized) => {
 4 |   return {
 5 |     type: ActionTypes.SET_ENTITIES,
 6 |     normalized,
 7 |   };
 8 | };
 9 | 
10 | export const removeEntities = (resource, ids) => {
11 |   return {
12 |     type: ActionTypes.REMOVE_ENTITIES_FROM_PAGE,
13 |     resource,
14 |     ids,
15 |   };
16 | };
17 | 


--------------------------------------------------------------------------------
/src/common/actions/errorActions.js:
--------------------------------------------------------------------------------
 1 | import Errors from '../constants/Errors';
 2 | import ActionTypes from '../constants/ActionTypes';
 3 | 
 4 | export const pushError = (error, meta) => {
 5 |   return {
 6 |     type: ActionTypes.PUSH_ERRORS,
 7 |     errors: [{
 8 |       ...error,
 9 |       meta,
10 |     }],
11 |   };
12 | };
13 | 
14 | export const pushErrors = (errors) => {
15 |   if (errors && errors.length === undefined) {
16 |     return pushError(Errors.UNKNOWN_EXCEPTION, errors);
17 |   }
18 |   return {
19 |     type: ActionTypes.PUSH_ERRORS,
20 |     errors,
21 |   };
22 | };
23 | 
24 | export const removeError = (id) => {
25 |   return {
26 |     type: ActionTypes.REMOVE_ERROR,
27 |     id,
28 |   };
29 | };
30 | 


--------------------------------------------------------------------------------
/src/common/actions/formActions.js:
--------------------------------------------------------------------------------
 1 | import formAPI from '../api/form';
 2 | import { pushErrors } from '../actions/errorActions';
 3 | 
 4 | export const validateForm = (formName, fieldName, value) => {
 5 |   return (dispatch, getState) => {
 6 |     return formAPI(getState().apiEngine)
 7 |       .form(formName)
 8 |       .field(fieldName, value)
 9 |       .validate()
10 |       .catch((err) => {
11 |         let validationError = {};
12 |         dispatch(pushErrors(err));
13 |         validationError[fieldName] = 'Unable to validate';
14 |         throw validationError;
15 |       });
16 |   };
17 | };
18 | 


--------------------------------------------------------------------------------
/src/common/actions/intlActions.js:
--------------------------------------------------------------------------------
 1 | import ActionTypes from '../constants/ActionTypes';
 2 | import localeAPI from '../api/locale';
 3 | import { setCookie } from './cookieActions';
 4 | 
 5 | export const updateLocale = (targetLocale) => {
 6 |   return (dispatch, getState) => {
 7 |     const currentLocale = getState().intl.locale;
 8 |     if (targetLocale === currentLocale) {
 9 |       return Promise.resolve();
10 |     }
11 |     return localeAPI(getState().apiEngine)
12 |       .read(targetLocale)
13 |       .then((json) => {
14 |         dispatch(setCookie('locale', json.locale));
15 |         dispatch({
16 |           type: ActionTypes.UPDATE_LOCALE,
17 |           locale: json.locale,
18 |           messages: json.messages,
19 |         });
20 |       }, (err) => {
21 |         dispatch(setCookie('locale', currentLocale));
22 |         return Promise.reject(err);
23 |       });
24 |   };
25 | };
26 | 


--------------------------------------------------------------------------------
/src/common/actions/pageActions.js:
--------------------------------------------------------------------------------
 1 | import ActionTypes from '../constants/ActionTypes';
 2 | import { setEntities } from './entityActions';
 3 | 
 4 | export const setPages = (resource, page, ids) => {
 5 |   return {
 6 |     type: ActionTypes.SET_PAGES,
 7 |     resource,
 8 |     page,
 9 |     ids,
10 |   };
11 | };
12 | 
13 | export const setCrrentPage = (resource, currentPage) => {
14 |   return {
15 |     type: ActionTypes.SET_CURRENT_PAGE,
16 |     resource,
17 |     currentPage,
18 |   };
19 | };
20 | 
21 | export const prependEntitiesIntoPage = (resource, normalized, intoPage) => {
22 |   return (dispatch, getState) => {
23 |     dispatch(setEntities(normalized));
24 |     dispatch({
25 |       type: ActionTypes.PREPEND_ENTITIES_INTO_PAGE,
26 |       resource,
27 |       ids: normalized.result,
28 |       intoPage,
29 |     });
30 |   };
31 | };
32 | 
33 | export const apendEntitiesIntoPage = (resource, normalized, intoPage) => {
34 |   return (dispatch, getState) => {
35 |     dispatch(setEntities(normalized));
36 |     dispatch({
37 |       type: ActionTypes.APPEND_ENTITIES_INTO_PAGE,
38 |       resource,
39 |       ids: normalized.result,
40 |       intoPage,
41 |     });
42 |   };
43 | };
44 | 


--------------------------------------------------------------------------------
/src/common/actions/routeActions.js:
--------------------------------------------------------------------------------
 1 | import { push } from 'react-router-redux';
 2 | import { setCookie } from './cookieActions';
 3 | 
 4 | export const redirect = (path) => {
 5 |   return (dispatch) => {
 6 |     dispatch(setCookie('redirect', path));
 7 |     dispatch(push(path));
 8 |   };
 9 | };
10 | 


--------------------------------------------------------------------------------
/src/common/actions/todoActions.js:
--------------------------------------------------------------------------------
 1 | import { normalize, arrayOf } from 'normalizr';
 2 | import { todoSchema } from '../schemas';
 3 | import Resources from '../constants/Resources';
 4 | import { setEntities, removeEntities } from './entityActions';
 5 | import { setPages, prependEntitiesIntoPage } from './pageActions';
 6 | 
 7 | export const setTodos = (res) => (dispatch, getState) => {
 8 |   let normalized = normalize(res.todos, arrayOf(todoSchema));
 9 | 
10 |   dispatch(setEntities(normalized));
11 |   dispatch(setPages(Resources.TODO, res.page, normalized.result));
12 | };
13 | 
14 | export const addTodo = (todo) => (dispatch, getState) => {
15 |   let normalized = normalize([todo], arrayOf(todoSchema));
16 | 
17 |   dispatch(prependEntitiesIntoPage(
18 |     Resources.TODO,
19 |     normalized,
20 |     1
21 |   ));
22 | };
23 | 
24 | export const removeTodo = (id) => removeEntities(Resources.TODO, [id]);
25 | 


--------------------------------------------------------------------------------
/src/common/actions/userActions.js:
--------------------------------------------------------------------------------
 1 | import { normalize, arrayOf } from 'normalizr';
 2 | import { userSchema } from '../schemas';
 3 | import Resources from '../constants/Resources';
 4 | import { setCookies, removeCookie } from './cookieActions';
 5 | import { setEntities } from './entityActions';
 6 | import { setPages } from './pageActions';
 7 | 
 8 | export const loginUser = ({ token, data }, res = null) => {
 9 |   return (dispatch) => {
10 |     return dispatch(setCookies({
11 |       token,
12 |       user: data,
13 |     }, res));
14 |   };
15 | };
16 | 
17 | export const logoutUser = () => {
18 |   return (dispatch) => Promise.all([
19 |     dispatch(removeCookie('token')),
20 |     dispatch(removeCookie('user')),
21 |   ]);
22 | };
23 | 
24 | export const setUsers = (res) => (dispatch, getState) => {
25 |   let normalized = normalize(res.users, arrayOf(userSchema));
26 | 
27 |   dispatch(setEntities(normalized));
28 |   dispatch(setPages(Resources.USER, res.page, normalized.result));
29 | };
30 | 


--------------------------------------------------------------------------------
/src/common/api/firebase.js:
--------------------------------------------------------------------------------
1 | export default (apiEngine) => ({
2 |   readToken: () => apiEngine.get('/api/users/me/firebase/token'),
3 | });
4 | 


--------------------------------------------------------------------------------
/src/common/api/form.js:
--------------------------------------------------------------------------------
 1 | export default (apiEngine) => ({
 2 |   form: (formName) => ({
 3 |     field: (fieldName, value) => ({
 4 |       validate: () => apiEngine.post(
 5 |         `/api/forms/${formName}/fields/${fieldName}/validation`, {
 6 |           data: { value },
 7 |         }
 8 |       ),
 9 |     }),
10 |   }),
11 | });
12 | 


--------------------------------------------------------------------------------
/src/common/api/locale.js:
--------------------------------------------------------------------------------
1 | export default (apiEngine) => ({
2 |   read: (locale) => apiEngine.get(`/api/locales/${locale}`),
3 | });
4 | 


--------------------------------------------------------------------------------
/src/common/api/todo.js:
--------------------------------------------------------------------------------
 1 | export default (apiEngine) => ({
 2 |   // list: () => new Promise((resolve, reject) => {
 3 |   //   setTimeout(() => {
 4 |   //     resolve(apiEngine.get('/api/todo'));
 5 |   //   }, 5000);
 6 |   // }),
 7 |   list: ({ page }) => apiEngine.get('/api/todos', { params: { page } }),
 8 |   create: (todo) => apiEngine.post('/api/todos', { data: todo }),
 9 |   update: (id, todo) => apiEngine.put(`/api/todos/${id}`, { data: todo }),
10 |   remove: (id) => apiEngine.del(`/api/todos/${id}`),
11 | });
12 | 


--------------------------------------------------------------------------------
/src/common/api/user.js:
--------------------------------------------------------------------------------
 1 | export default (apiEngine) => ({
 2 |   list: ({ page }) => apiEngine.get('/api/users', { params: { page } }),
 3 |   register: (user) => apiEngine.post('/api/users', { data: user }),
 4 |   verifyEmail: ({ token }) => apiEngine.post('/api/users/email/verify', {
 5 |     data: { verifyEmailToken: token },
 6 |   }),
 7 |   requestVerifyEmail: (form) => (
 8 |     apiEngine.post('/api/users/email/request-verify', { data: form })
 9 |   ),
10 |   login: (user) => apiEngine.post('/api/users/login', { data: user }),
11 |   requestResetPassword: (form) => (
12 |     apiEngine.post('/api/users/password/request-reset', { data: form })
13 |   ),
14 |   resetPassword: ({ token, ...form }) => (
15 |     apiEngine.put('/api/users/password', {
16 |       data: {
17 |         resetPasswordToken: token,
18 |         ...form,
19 |       },
20 |     })
21 |   ),
22 |   logout: () => apiEngine.get('/api/users/logout'),
23 |   readSelf: () => apiEngine.get('/api/users/me'),
24 |   update: (user) => apiEngine.put('/api/users/me', { data: user }),
25 |   updateAvatarURL: (form) => apiEngine.put('/api/users/me/avatarURL', {
26 |     data: form,
27 |   }),
28 |   updatePassword: (form) => apiEngine.put('/api/users/me/password', {
29 |     data: form,
30 |   }),
31 |   uploadAvatar: (avatar) =>
32 |     apiEngine.post('/api/users/me/avatar', { files: { avatar } }),
33 | });
34 | 


--------------------------------------------------------------------------------
/src/common/components/fields/adapters/BsCheckbox.js:
--------------------------------------------------------------------------------
 1 | import React, { PropTypes } from 'react';
 2 | import Checkbox from '../bases/Checkbox';
 3 | 
 4 | let BsCheckbox = ({ input, ...rest }) => (
 5 |   
6 | 10 |
11 | ); 12 | 13 | BsCheckbox.propTypes = { 14 | input: PropTypes.object.isRequired, 15 | }; 16 | 17 | export default BsCheckbox; 18 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/BsCheckboxList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import CheckboxListItem from '../bases/CheckboxListItem'; 3 | 4 | let BsCheckboxList = ({ input, options, ...rest }) => ( 5 | 6 | {options.map((option, index) => ( 7 |
12 | 17 |
18 | ))} 19 |
20 | ); 21 | 22 | BsCheckboxList.propTypes = { 23 | input: PropTypes.object.isRequired, 24 | options: PropTypes.arrayOf( 25 | PropTypes.shape({ 26 | label: PropTypes.string, 27 | value: PropTypes.oneOfType([ 28 | PropTypes.string, 29 | PropTypes.number, 30 | ]), 31 | }) 32 | ), 33 | }; 34 | 35 | export default BsCheckboxList; 36 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/BsInput.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | import Input from '../bases/Input'; 4 | 5 | let BsInput = ({ input, className, ...rest }) => ( 6 | 11 | ); 12 | 13 | BsInput.propTypes = { 14 | input: PropTypes.object.isRequired, 15 | className: PropTypes.string, 16 | }; 17 | 18 | export default BsInput; 19 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/BsPlaintext.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | import Plaintext from '../bases/Plaintext'; 4 | 5 | let BsPlaintext = ({ input, className, ...rest }) => ( 6 | 11 | ); 12 | 13 | BsPlaintext.propTypes = { 14 | input: PropTypes.object.isRequired, 15 | className: PropTypes.string, 16 | }; 17 | 18 | export default BsPlaintext; 19 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/BsRadio.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import RadioItem from '../bases/RadioItem'; 3 | 4 | let BsRadio = ({ input, options, ...rest }) => ( 5 | <span> 6 | {options.map((option, index) => ( 7 | <div 8 | className="radio" 9 | key={option.value} 10 | {...rest} 11 | > 12 | <RadioItem 13 | input={input} 14 | option={option} 15 | /> 16 | </div> 17 | ))} 18 | </span> 19 | ); 20 | 21 | BsRadio.propTypes = { 22 | input: PropTypes.object.isRequired, 23 | options: PropTypes.arrayOf( 24 | PropTypes.shape({ 25 | label: PropTypes.string, 26 | value: PropTypes.oneOfType([ 27 | PropTypes.string, 28 | PropTypes.number, 29 | ]), 30 | }) 31 | ), 32 | }; 33 | 34 | export default BsRadio; 35 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/BsSelect.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | import Select from '../bases/Select'; 4 | 5 | let BsSelect = ({ input, className, ...rest }) => ( 6 | <Select 7 | className={cx('form-control', className)} 8 | input={input} 9 | {...rest} 10 | /> 11 | ); 12 | 13 | BsSelect.propTypes = { 14 | input: PropTypes.object.isRequired, 15 | className: PropTypes.string, 16 | }; 17 | 18 | export default BsSelect; 19 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/BsTextarea.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import cx from 'classnames'; 3 | import Textarea from '../bases/Textarea'; 4 | 5 | let BsTextarea = ({ input, className, ...rest }) => ( 6 | <Textarea 7 | className={cx('form-control', className)} 8 | input={input} 9 | {...rest} 10 | /> 11 | ); 12 | 13 | BsTextarea.propTypes = { 14 | input: PropTypes.object.isRequired, 15 | className: PropTypes.string, 16 | }; 17 | 18 | export default BsTextarea; 19 | -------------------------------------------------------------------------------- /src/common/components/fields/adapters/index.js: -------------------------------------------------------------------------------- 1 | export { default as BsInput } from './BsInput'; 2 | export { default as BsTextarea } from './BsTextarea'; 3 | export { default as BsPlaintext } from './BsPlaintext'; 4 | export { default as BsSelect } from './BsSelect'; 5 | export { default as BsCheckbox } from './BsCheckbox'; 6 | export { default as BsCheckboxList } from './BsCheckboxList'; 7 | export { default as BsRadio } from './BsRadio'; 8 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/AirDateRange.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { DateRangePicker } from 'react-dates'; 3 | import moment from 'moment'; 4 | import 'react-dates/lib/css/_datepicker.css'; 5 | 6 | let defaultValue = { 7 | startDate: null, 8 | endDate: null, 9 | }; 10 | 11 | class AirDateRange extends Component { 12 | constructor() { 13 | super(); 14 | this.state = { 15 | focusedInput: null, 16 | }; 17 | this.onDatesChange = this._onDatesChange.bind(this); 18 | this.onFocusChange = this._onFocusChange.bind(this); 19 | } 20 | 21 | _onDatesChange({ startDate, endDate }) { 22 | this.props.input.onChange({ startDate, endDate }); 23 | } 24 | 25 | _onFocusChange(focusedInput) { 26 | this.setState({ focusedInput }); 27 | } 28 | 29 | render() { 30 | let { 31 | input, 32 | ...rest 33 | } = this.props; 34 | let { focusedInput } = this.state; 35 | 36 | return ( 37 | <DateRangePicker 38 | {...rest} 39 | onDatesChange={this.onDatesChange} 40 | onFocusChange={this.onFocusChange} 41 | focusedInput={focusedInput} 42 | startDate={ 43 | input.value.startDate ? 44 | moment(input.value.startDate) : 45 | defaultValue.startDate 46 | } 47 | endDate={ 48 | input.value.endDate ? 49 | moment(input.value.endDate) : 50 | defaultValue.endDate 51 | } 52 | /> 53 | ); 54 | } 55 | } 56 | 57 | AirDateRange.propTypes = { 58 | input: PropTypes.object.isRequired, 59 | }; 60 | 61 | export default AirDateRange; 62 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/AirSingleDate.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { SingleDatePicker } from 'react-dates'; 3 | import moment from 'moment'; 4 | import 'react-dates/lib/css/_datepicker.css'; 5 | 6 | let defaultValue = null; 7 | 8 | class AirSingleDate extends Component { 9 | constructor() { 10 | super(); 11 | this.state = { 12 | focused: false, 13 | }; 14 | this.onDateChange = this._onDateChange.bind(this); 15 | this.onFocusChange = this._onFocusChange.bind(this); 16 | } 17 | 18 | _onDateChange(date) { 19 | this.props.input.onChange(date); 20 | } 21 | 22 | _onFocusChange({ focused }) { 23 | this.setState({ focused }); 24 | } 25 | 26 | render() { 27 | let { 28 | input, 29 | ...rest 30 | } = this.props; 31 | let { focused } = this.state; 32 | 33 | return ( 34 | <SingleDatePicker 35 | numberOfMonths={1} 36 | {...rest} 37 | id="date_input" 38 | focused={focused} 39 | date={input.value ? moment(input.value) : defaultValue} 40 | onDateChange={this.onDateChange} 41 | onFocusChange={this.onFocusChange} 42 | /> 43 | ); 44 | } 45 | } 46 | 47 | AirSingleDate.propTypes = { 48 | input: PropTypes.object.isRequired, 49 | }; 50 | 51 | export default AirSingleDate; 52 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | let Checkbox = ({ input, text, ...rest }) => ( 4 | <label> 5 | <input 6 | {...input} 7 | type="checkbox" 8 | checked={input.value} 9 | {...rest} 10 | /> {text} 11 | </label> 12 | ); 13 | 14 | Checkbox.propTypes = { 15 | input: PropTypes.object.isRequired, 16 | text: PropTypes.node, 17 | }; 18 | 19 | export default Checkbox; 20 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/CheckboxListItem.js: -------------------------------------------------------------------------------- 1 | // ref: 2 | // - <https://github.com/erikras/redux-form/issues/1037> 3 | import React, { Component, PropTypes } from 'react'; 4 | 5 | class CheckboxListItem extends Component { 6 | constructor() { 7 | super(); 8 | this.onChange = this._onChange.bind(this); 9 | } 10 | 11 | _onChange(e) { 12 | let { input, option: { value } } = this.props; 13 | let newValue = [...input.value]; 14 | 15 | if (e.target.checked) { 16 | newValue.push(value); 17 | } else { 18 | newValue.splice(newValue.indexOf(value), 1); 19 | } 20 | 21 | return input.onChange(newValue); 22 | } 23 | 24 | render() { 25 | let { input, index, option } = this.props; 26 | let { label, value, ...optionProps } = option; 27 | 28 | return ( 29 | <label> 30 | <input 31 | type="checkbox" 32 | name={`${input.name}[${index}]`} 33 | value={value} 34 | checked={input.value.indexOf(value) !== -1} 35 | onChange={this.onChange} 36 | {...optionProps} 37 | /> {label} 38 | </label> 39 | ); 40 | } 41 | }; 42 | 43 | CheckboxListItem.propTypes = { 44 | input: PropTypes.object.isRequired, 45 | index: PropTypes.number, 46 | option: PropTypes.shape({ 47 | label: PropTypes.string, 48 | value: PropTypes.oneOfType([ 49 | PropTypes.string, 50 | PropTypes.number, 51 | ]), 52 | }), 53 | }; 54 | 55 | export default CheckboxListItem; 56 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/Input.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | let Input = ({ input, type, ...rest }) => ( 4 | <input 5 | {...input} 6 | type={type} 7 | {...rest} 8 | /> 9 | ); 10 | 11 | Input.propTypes = { 12 | input: PropTypes.object.isRequired, 13 | type: PropTypes.oneOf([ 14 | 'text', 15 | 'password', 16 | 'number', 17 | 'date', 18 | 'time', 19 | 'file', 20 | ]), 21 | }; 22 | 23 | export default Input; 24 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/Plaintext.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | let Plaintext = ({ input, text, ...rest }) => ( 4 | <p {...rest}> 5 | {text} 6 | </p> 7 | ); 8 | 9 | Plaintext.propTypes = { 10 | input: PropTypes.object.isRequired, 11 | text: PropTypes.string, 12 | }; 13 | 14 | export default Plaintext; 15 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/RadioItem.js: -------------------------------------------------------------------------------- 1 | // ref: 2 | // - <https://github.com/erikras/redux-form/issues/1857#issuecomment-249890206> 3 | import React, { PropTypes } from 'react'; 4 | 5 | let RadioItem = ({ input, option }) => { 6 | let { label, value, ...optionProps } = option; 7 | 8 | return ( 9 | <label> 10 | <input 11 | {...input} 12 | type="radio" 13 | name={`${input.name}_${value}`} 14 | value={value} 15 | checked={value === input.value} 16 | {...optionProps} 17 | /> {label} 18 | </label> 19 | ); 20 | }; 21 | 22 | RadioItem.propTypes = { 23 | input: PropTypes.object.isRequired, 24 | option: PropTypes.shape({ 25 | label: PropTypes.string, 26 | value: PropTypes.oneOfType([ 27 | PropTypes.string, 28 | PropTypes.number, 29 | ]), 30 | }), 31 | }; 32 | 33 | export default RadioItem; 34 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/RangeSlider/RangeSlider.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactSlider from 'react-slider'; 3 | import './RangeSlider.scss'; 4 | 5 | let defaultValue = { 6 | min: 0, 7 | max: 0, 8 | }; 9 | 10 | class RangeSlider extends Component { 11 | render() { 12 | let { 13 | input, 14 | ...rest 15 | } = this.props; 16 | 17 | return ( 18 | <ReactSlider 19 | {...rest} 20 | onChange={(value) => { 21 | input.onChange({ 22 | min: value[0], 23 | max: value[1], 24 | }); 25 | }} 26 | value={[ 27 | input.value.min || defaultValue.min, 28 | input.value.max || defaultValue.max, 29 | ]} 30 | withBars 31 | > 32 | <div className="slider-handle"></div> 33 | <div className="slider-handle"></div> 34 | </ReactSlider> 35 | ); 36 | } 37 | } 38 | 39 | RangeSlider.propTypes = { 40 | input: PropTypes.object.isRequired, 41 | }; 42 | 43 | export default RangeSlider; 44 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/RangeSlider/RangeSlider.scss: -------------------------------------------------------------------------------- 1 | .slider { 2 | height: 5px; 3 | background-color: #ddd; 4 | position: relative; 5 | top: 5px; 6 | border-radius: 2px; 7 | margin: 12px 0; 8 | cursor: pointer; 9 | 10 | .bar-1 { 11 | height: 5px; 12 | background-color: rgb(38, 166, 154); 13 | border-radius: 5px; 14 | } 15 | 16 | .slider-handle { 17 | position: relative; 18 | top: -5px; 19 | border-radius: 50%; 20 | height: 15px; 21 | width: 15px; 22 | background-color: rgb(38, 166, 154); 23 | cursor: pointer; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/Recaptcha.js: -------------------------------------------------------------------------------- 1 | // ref: 2 | // - <https://github.com/erikras/redux-form/issues/1880> 3 | import React, { PropTypes } from 'react'; 4 | import GoogleRecaptcha from 'react-google-recaptcha'; 5 | import configs from '../../../../../configs/project/client'; 6 | 7 | let Recaptcha = ({ input, disableHint, ...rest }) => ( 8 | configs.recaptcha ? ( 9 | <GoogleRecaptcha 10 | sitekey={configs.recaptcha[process.env.NODE_ENV].siteKey} 11 | onChange={input.onChange} 12 | {...rest} 13 | /> 14 | ) : ( 15 | <span> 16 | {disableHint} 17 | </span> 18 | ) 19 | ); 20 | 21 | Recaptcha.propTypes = { 22 | input: PropTypes.object.isRequired, 23 | disableHint: PropTypes.node, 24 | }; 25 | 26 | Recaptcha.defaultProps = { 27 | disableHint: ( 28 | <pre> 29 | Recaptcha is disabled 30 | </pre> 31 | ), 32 | }; 33 | 34 | export default Recaptcha; 35 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/Select.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | let Select = ({ input, options, ...rest }) => ( 4 | <select {...input} {...rest}> 5 | {options.map(({ label, ...optionProps }) => ( 6 | <option 7 | key={optionProps.value} 8 | {...optionProps} 9 | > 10 | {label} 11 | </option> 12 | ))} 13 | </select> 14 | ); 15 | 16 | Select.propTypes = { 17 | input: PropTypes.object.isRequired, 18 | options: PropTypes.arrayOf( 19 | PropTypes.shape({ 20 | label: PropTypes.string, 21 | value: PropTypes.oneOfType([ 22 | PropTypes.string, 23 | PropTypes.number, 24 | ]), 25 | }) 26 | ), 27 | }; 28 | 29 | export default Select; 30 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/Textarea.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | let Textarea = ({ input, ...rest }) => ( 4 | <textarea 5 | {...input} 6 | {...rest} 7 | /> 8 | ); 9 | 10 | Textarea.propTypes = { 11 | input: PropTypes.object.isRequired, 12 | }; 13 | 14 | export default Textarea; 15 | -------------------------------------------------------------------------------- /src/common/components/fields/bases/index.js: -------------------------------------------------------------------------------- 1 | export { default as Input } from './Input'; 2 | export { default as Textarea } from './Textarea'; 3 | export { default as Plaintext } from './Plaintext'; 4 | export { default as RangeSlider } from './RangeSlider/RangeSlider'; 5 | export { default as AirSingleDate } from './AirSingleDate'; 6 | export { default as AirDateRange } from './AirDateRange'; 7 | export { default as Select } from './Select'; 8 | export { default as Checkbox } from './Checkbox'; 9 | export { default as CheckboxListItem } from './CheckboxListItem'; 10 | export { default as RadioItem } from './RadioItem'; 11 | export { default as Recaptcha } from './Recaptcha'; 12 | -------------------------------------------------------------------------------- /src/common/components/fields/widgets/BsField.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import React, { PropTypes } from 'react'; 3 | import BsFormGroup from 'react-bootstrap/lib/FormGroup'; 4 | import Col from 'react-bootstrap/lib/Col'; 5 | import Row from 'react-bootstrap/lib/Row'; 6 | import ControlLabel from 'react-bootstrap/lib/ControlLabel'; 7 | import HelpBlock from 'react-bootstrap/lib/HelpBlock'; 8 | 9 | let BsField = ({ 10 | horizontal, labelDimensions, fieldDimensions, showLabel, label, adapter, 11 | meta, ...rest 12 | }, { 13 | defaultHorizontal, defaultLabelDimensions, defaultFieldDimensions, 14 | }) => { 15 | let isShowError = meta && meta.touched && meta.error; 16 | let Adapter = adapter; 17 | let renderedFormControl = ( 18 | <Adapter {...rest} /> 19 | ); 20 | 21 | horizontal = (horizontal === undefined) ? defaultHorizontal : horizontal; 22 | labelDimensions = assign({}, defaultLabelDimensions, labelDimensions || {}); 23 | fieldDimensions = assign({}, defaultFieldDimensions, fieldDimensions || {}); 24 | 25 | return horizontal ? ( 26 | <BsFormGroup validationState={isShowError ? 'error' : undefined}> 27 | {showLabel && ( 28 | <Col componentClass={ControlLabel} {...labelDimensions}> 29 | {label} 30 | </Col> 31 | )} 32 | <Col {...fieldDimensions}> 33 | {renderedFormControl} 34 | {isShowError && ( 35 | <HelpBlock>{meta.error}</HelpBlock> 36 | )} 37 | </Col> 38 | </BsFormGroup> 39 | ) : ( 40 | <BsFormGroup validationState={isShowError ? 'error' : undefined}> 41 | {showLabel && ( 42 | <Row> 43 | <Col componentClass={ControlLabel} {...labelDimensions}> 44 | <ControlLabel>{label}</ControlLabel> 45 | </Col> 46 | </Row> 47 | )} 48 | <Row> 49 | <Col {...fieldDimensions}> 50 | {renderedFormControl} 51 | {isShowError && ( 52 | <HelpBlock>{meta.error}</HelpBlock> 53 | )} 54 | </Col> 55 | </Row> 56 | </BsFormGroup> 57 | ); 58 | }; 59 | 60 | BsField.propTypes = { 61 | horizontal: PropTypes.bool, 62 | labelDimensions: PropTypes.object, 63 | fieldDimensions: PropTypes.object, 64 | showLabel: PropTypes.bool, 65 | label: PropTypes.string, 66 | }; 67 | 68 | BsField.contextTypes = { 69 | defaultHorizontal: PropTypes.bool, 70 | defaultLabelDimensions: PropTypes.object, 71 | defaultFieldDimensions: PropTypes.object, 72 | }; 73 | 74 | BsField.defaultProps = { 75 | showLabel: true, 76 | label: '', 77 | }; 78 | 79 | export default BsField; 80 | -------------------------------------------------------------------------------- /src/common/components/fields/widgets/BsForm.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import Form from 'react-bootstrap/lib/Form'; 3 | 4 | class BsForm extends Component { 5 | getChildContext() { 6 | let { 7 | defaultHorizontal, 8 | defaultLabelDimensions, 9 | defaultFieldDimensions, 10 | } = this.props; 11 | 12 | return { 13 | defaultHorizontal, 14 | defaultLabelDimensions, 15 | defaultFieldDimensions, 16 | }; 17 | } 18 | 19 | render() { 20 | let { 21 | /* eslint-disable */ 22 | // consume props owned by BsForm 23 | defaultHorizontal, defaultLabelDimensions, defaultFieldDimensions, 24 | /* eslint-enable */ 25 | children, 26 | ...rest 27 | } = this.props; 28 | 29 | return ( 30 | <Form horizontal={defaultHorizontal} {...rest}> 31 | {children} 32 | </Form> 33 | ); 34 | } 35 | }; 36 | 37 | BsForm.propTypes = { 38 | defaultHorizontal: PropTypes.bool, 39 | defaultLabelDimensions: PropTypes.object, 40 | defaultFieldDimensions: PropTypes.object, 41 | }; 42 | 43 | BsForm.childContextTypes = { 44 | defaultHorizontal: PropTypes.bool, 45 | defaultLabelDimensions: PropTypes.object, 46 | defaultFieldDimensions: PropTypes.object, 47 | }; 48 | 49 | BsForm.defaultProps = { 50 | defaultHorizontal: true, 51 | defaultLabelDimensions: { 52 | sm: 2, 53 | }, 54 | defaultFieldDimensions: { 55 | sm: 10, 56 | }, 57 | }; 58 | 59 | export default BsForm; 60 | -------------------------------------------------------------------------------- /src/common/components/fields/widgets/BsFormFooter.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import React, { PropTypes } from 'react'; 3 | import BsFormGroup from 'react-bootstrap/lib/FormGroup'; 4 | import Col from 'react-bootstrap/lib/Col'; 5 | import ControlLabel from 'react-bootstrap/lib/ControlLabel'; 6 | 7 | let BsFormFooter = ({ 8 | horizontal, labelDimensions, fieldDimensions, showLabel, label, children, 9 | }, { 10 | defaultHorizontal, defaultLabelDimensions, defaultFieldDimensions, 11 | }) => { 12 | horizontal = (horizontal === undefined) ? defaultHorizontal : horizontal; 13 | labelDimensions = assign({}, defaultLabelDimensions, labelDimensions || {}); 14 | fieldDimensions = assign({}, defaultFieldDimensions, fieldDimensions || {}); 15 | 16 | return horizontal ? ( 17 | <BsFormGroup> 18 | {showLabel && ( 19 | <Col componentClass={ControlLabel} {...labelDimensions}> 20 | {label} 21 | </Col> 22 | )} 23 | <Col {...fieldDimensions}> 24 | {children} 25 | </Col> 26 | </BsFormGroup> 27 | ) : ( 28 | <BsFormGroup> 29 | {children} 30 | </BsFormGroup> 31 | ); 32 | }; 33 | 34 | BsFormFooter.propTypes = { 35 | horizontal: PropTypes.bool, 36 | labelDimensions: PropTypes.object, 37 | fieldDimensions: PropTypes.object, 38 | showLabel: PropTypes.bool, 39 | label: PropTypes.string, 40 | }; 41 | 42 | BsFormFooter.contextTypes = { 43 | defaultLabelDimensions: PropTypes.object, 44 | defaultFieldDimensions: PropTypes.object, 45 | defaultHorizontal: PropTypes.bool, 46 | }; 47 | 48 | BsFormFooter.defaultProps = { 49 | showLabel: true, 50 | label: '', 51 | }; 52 | 53 | export default BsFormFooter; 54 | -------------------------------------------------------------------------------- /src/common/components/fields/widgets/index.js: -------------------------------------------------------------------------------- 1 | export { default as BsForm } from './BsForm'; 2 | export { default as BsField } from './BsField'; 3 | export { default as BsFormFooter } from './BsFormFooter'; 4 | -------------------------------------------------------------------------------- /src/common/components/forms/user/EditForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Field, reduxForm } from 'redux-form'; 4 | import Alert from 'react-bootstrap/lib/Alert'; 5 | import Button from 'react-bootstrap/lib/Button'; 6 | import FormNames from '../../../constants/FormNames'; 7 | import userAPI from '../../../api/user'; 8 | import { pushErrors } from '../../../actions/errorActions'; 9 | import { setCookies } from '../../../actions/cookieActions'; 10 | import { BsInput as Input } from '../../fields/adapters'; 11 | import { 12 | BsForm as Form, 13 | BsFormFooter as FormFooter, 14 | BsField as FormField, 15 | } from '../../fields/widgets'; 16 | export const validate = (values) => { 17 | const errors = {}; 18 | 19 | if (!values.name) { 20 | errors.name = 'Required'; 21 | } 22 | 23 | return errors; 24 | }; 25 | 26 | class EditForm extends Component { 27 | constructor(props) { 28 | super(props); 29 | this.init = this._init.bind(this); 30 | this.handleSubmit = this._handleSubmit.bind(this); 31 | } 32 | 33 | _init(user) { 34 | let { initialize } = this.props; 35 | 36 | initialize({ 37 | name: user.name, 38 | }); 39 | } 40 | 41 | componentDidMount() { 42 | let { dispatch, apiEngine } = this.props; 43 | 44 | userAPI(apiEngine) 45 | .readSelf() 46 | .catch((err) => { 47 | dispatch(pushErrors(err)); 48 | throw err; 49 | }) 50 | .then((json) => { 51 | this.init(json.user); 52 | }); 53 | } 54 | 55 | _handleSubmit(formData) { 56 | let { dispatch, apiEngine } = this.props; 57 | 58 | return userAPI(apiEngine) 59 | .update(formData) 60 | .catch((err) => { 61 | dispatch(pushErrors(err)); 62 | throw err; 63 | }) 64 | .then((json) => { 65 | this.init(json.user); 66 | dispatch(setCookies({ 67 | user: json.user, 68 | })); 69 | }); 70 | } 71 | 72 | render() { 73 | const { 74 | handleSubmit, 75 | submitSucceeded, 76 | submitFailed, 77 | error, 78 | pristine, 79 | submitting, 80 | invalid, 81 | } = this.props; 82 | 83 | return ( 84 | <Form onSubmit={handleSubmit(this.handleSubmit)}> 85 | {submitSucceeded && (<Alert bsStyle="success">Profile Saved</Alert>)} 86 | {submitFailed && error && (<Alert bsStyle="danger">{error}</Alert>)} 87 | <Field 88 | name="name" 89 | component={FormField} 90 | label="Name" 91 | adapter={Input} 92 | type="text" 93 | placeholder="Name" 94 | /> 95 | <FormFooter> 96 | <Button type="submit" disabled={pristine || submitting || invalid}> 97 | Save 98 | {submitting && ( 99 | <i className="fa fa-spinner fa-spin" aria-hidden="true" /> 100 | )} 101 | </Button> 102 | </FormFooter> 103 | </Form> 104 | ); 105 | } 106 | }; 107 | 108 | export default reduxForm({ 109 | form: FormNames.USER_EDIT, 110 | validate, 111 | })(connect(state => ({ 112 | apiEngine: state.apiEngine, 113 | }))(EditForm)); 114 | -------------------------------------------------------------------------------- /src/common/components/forms/user/LoginForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import { push } from 'react-router-redux'; 5 | import { Field, reduxForm, SubmissionError } from 'redux-form'; 6 | import Alert from 'react-bootstrap/lib/Alert'; 7 | import Button from 'react-bootstrap/lib/Button'; 8 | // import validator from 'validator'; 9 | import FormNames from '../../../constants/FormNames'; 10 | import userAPI from '../../../api/user'; 11 | import { pushErrors } from '../../../actions/errorActions'; 12 | import { loginUser } from '../../../actions/userActions'; 13 | import { BsInput as Input } from '../../fields/adapters'; 14 | import { 15 | BsForm as Form, 16 | BsFormFooter as FormFooter, 17 | BsField as FormField, 18 | } from '../../fields/widgets'; 19 | 20 | const validate = (values) => { 21 | const errors = {}; 22 | 23 | // if (values.email && !validator.isEmail(values.email)) { 24 | // errors.email = 'Not an email'; 25 | // } 26 | 27 | if (!values.email) { 28 | errors.email = 'Required'; 29 | } 30 | 31 | if (!values.password) { 32 | errors.password = 'Required'; 33 | } 34 | 35 | return errors; 36 | }; 37 | 38 | class LoginForm extends Component { 39 | constructor(props) { 40 | super(props); 41 | this.handleSubmit = this._handleSubmit.bind(this); 42 | } 43 | 44 | _handleSubmit(formData) { 45 | let { dispatch, apiEngine, change } = this.props; 46 | 47 | return userAPI(apiEngine) 48 | .login(formData) 49 | .catch((err) => { 50 | dispatch(pushErrors(err)); 51 | throw err; 52 | }) 53 | .then((json) => { 54 | if (json.isAuth) { 55 | // redirect to the origin path before logging in 56 | let { next } = this.props.routing.locationBeforeTransitions.query; 57 | 58 | dispatch(loginUser({ 59 | token: json.token, 60 | data: json.user, 61 | })); 62 | dispatch(push(next || '/')); 63 | } else { 64 | change('password', ''); 65 | throw new SubmissionError({ 66 | _error: 'Login failed. You may type wrong email or password.', 67 | }); 68 | } 69 | }); 70 | } 71 | 72 | render() { 73 | const { 74 | handleSubmit, 75 | submitFailed, 76 | error, 77 | pristine, 78 | submitting, 79 | invalid, 80 | } = this.props; 81 | 82 | return ( 83 | <Form onSubmit={handleSubmit(this.handleSubmit)}> 84 | {submitFailed && error && (<Alert bsStyle="danger">{error}</Alert>)} 85 | <Field 86 | name="email" 87 | component={FormField} 88 | label="Email" 89 | adapter={Input} 90 | type="text" 91 | placeholder="Email" 92 | /> 93 | <Field 94 | name="password" 95 | component={FormField} 96 | label="Password" 97 | adapter={Input} 98 | type="password" 99 | placeholder="Password" 100 | /> 101 | <FormFooter> 102 | <Button type="submit" disabled={pristine || submitting || invalid}> 103 | Login 104 | </Button> 105 | <Link to="/user/password/forget"> 106 | <Button bsStyle="link">Forget password?</Button> 107 | </Link> 108 | </FormFooter> 109 | </Form> 110 | ); 111 | } 112 | }; 113 | 114 | export default reduxForm({ 115 | form: FormNames.USER_LOGIN, 116 | validate, 117 | })(connect(state => ({ 118 | apiEngine: state.apiEngine, 119 | routing: state.routing, 120 | }))(LoginForm)); 121 | -------------------------------------------------------------------------------- /src/common/components/forms/user/ResetPasswordForm.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import { Field, reduxForm } from 'redux-form'; 5 | import Alert from 'react-bootstrap/lib/Alert'; 6 | import Button from 'react-bootstrap/lib/Button'; 7 | import FormNames from '../../../constants/FormNames'; 8 | import userAPI from '../../../api/user'; 9 | import { pushErrors } from '../../../actions/errorActions'; 10 | import { BsInput as Input } from '../../fields/adapters'; 11 | import { 12 | BsForm as Form, 13 | BsFormFooter as FormFooter, 14 | BsField as FormField, 15 | } from '../../fields/widgets'; 16 | 17 | export const validate = (values) => { 18 | const errors = {}; 19 | 20 | if ( 21 | values.newPasswordConfirm && 22 | values.newPassword !== values.newPasswordConfirm 23 | ) { 24 | errors.newPassword = errors.newPasswordConfirm = 'Password Not Matched'; 25 | } 26 | 27 | if (!values.newPassword) { 28 | errors.newPassword = 'Required'; 29 | } 30 | 31 | if (!values.newPasswordConfirm) { 32 | errors.newPasswordConfirm = 'Required'; 33 | } 34 | 35 | return errors; 36 | }; 37 | 38 | class ChangePasswordForm extends Component { 39 | constructor(props) { 40 | super(props); 41 | this.handleSubmit = this._handleSubmit.bind(this); 42 | } 43 | 44 | _handleSubmit(formData) { 45 | let { dispatch, apiEngine, routing, initialize } = this.props; 46 | let location = routing.locationBeforeTransitions; 47 | 48 | return userAPI(apiEngine) 49 | .resetPassword({ 50 | ...formData, 51 | token: location.query.token, 52 | }) 53 | .catch((err) => { 54 | dispatch(pushErrors(err)); 55 | throw err; 56 | }) 57 | .then((json) => { 58 | initialize({ 59 | newPassword: '', 60 | newPasswordConfirm: '', 61 | }); 62 | }); 63 | } 64 | 65 | render() { 66 | const { 67 | handleSubmit, 68 | submitSucceeded, 69 | submitFailed, 70 | error, 71 | pristine, 72 | submitting, 73 | invalid, 74 | } = this.props; 75 | 76 | return ( 77 | <Form onSubmit={handleSubmit(this.handleSubmit)}> 78 | {submitSucceeded && ( 79 | <Alert bsStyle="success"> 80 | Password Changed. 81 | Go to <Link to="/user/login">login page</Link> now. 82 | </Alert> 83 | )} 84 | {submitFailed && error && (<Alert bsStyle="danger">{error}</Alert>)} 85 | <Field 86 | name="newPassword" 87 | component={FormField} 88 | label="New Password" 89 | adapter={Input} 90 | type="password" 91 | disabled={submitSucceeded} 92 | placeholder="New Password" 93 | /> 94 | <Field 95 | name="newPasswordConfirm" 96 | component={FormField} 97 | label="New Password Confirm" 98 | adapter={Input} 99 | type="password" 100 | disabled={submitSucceeded} 101 | placeholder="New Password Confirm" 102 | /> 103 | <FormFooter> 104 | <Button type="submit" disabled={pristine || submitting || invalid}> 105 | Reset 106 | {submitting && ( 107 | <i className="fa fa-spinner fa-spin" aria-hidden="true" /> 108 | )} 109 | </Button> 110 | </FormFooter> 111 | </Form> 112 | ); 113 | } 114 | }; 115 | 116 | export default reduxForm({ 117 | form: FormNames.USER_RESET_PASSWORD, 118 | validate, 119 | })(connect(state => ({ 120 | apiEngine: state.apiEngine, 121 | routing: state.routing, 122 | }))(ChangePasswordForm)); 123 | -------------------------------------------------------------------------------- /src/common/components/layouts/AdminPageLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Navbar from 'react-bootstrap/lib/Navbar'; 3 | import Grid from 'react-bootstrap/lib/Grid'; 4 | import Row from 'react-bootstrap/lib/Row'; 5 | import Col from 'react-bootstrap/lib/Col'; 6 | import Nav from 'react-bootstrap/lib/Nav'; 7 | import NavItem from 'react-bootstrap/lib/NavItem'; 8 | import ErrorList from '../utils/ErrorList'; 9 | 10 | const AdminPageLayout = ({ children, ...rest }) => ( 11 | <div> 12 | <Navbar fluid> 13 | <Navbar.Header> 14 | <Navbar.Brand> 15 | <a href="/admin">Express-React-HMR-Boilerplate Admin System</a> 16 | </Navbar.Brand> 17 | <Navbar.Toggle /> 18 | </Navbar.Header> 19 | <Navbar.Collapse> 20 | </Navbar.Collapse> 21 | </Navbar> 22 | <Grid fluid> 23 | <Row> 24 | <Col md={2}> 25 | <Nav bsStyle="pills" stacked activeKey={1}> 26 | <NavItem eventKey={1} href="/admin/user">User</NavItem> 27 | <NavItem eventKey={2} href="/">Go back to site</NavItem> 28 | </Nav> 29 | </Col> 30 | <Col md={10} {...rest}> 31 | <ErrorList /> 32 | {children} 33 | </Col> 34 | </Row> 35 | </Grid> 36 | </div> 37 | ); 38 | 39 | export default AdminPageLayout; 40 | -------------------------------------------------------------------------------- /src/common/components/layouts/AppLayout.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Head from '../widgets/Head'; 3 | 4 | const AppLayout = ({ children }) => ( 5 | <div> 6 | <Head 7 | title="Express-React-HMR-Boilerplate" 8 | metas={[ 9 | {charset: 'utf-8'}, 10 | { 11 | name: 'viewport', 12 | content: 'width=device-width, initial-scale=1.0', 13 | }, 14 | ]} 15 | links={[ 16 | 'https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css', 17 | '/css/main.css', 18 | ]} 19 | scripts={[ 20 | 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js', 21 | ]} 22 | /> 23 | {children} 24 | </div> 25 | ); 26 | 27 | export default AppLayout; 28 | -------------------------------------------------------------------------------- /src/common/components/layouts/PageLayout.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Grid from 'react-bootstrap/lib/Grid'; 3 | import Navigation from '../utils/Navigation'; 4 | import ErrorList from '../utils/ErrorList'; 5 | 6 | let PageLayout = ({ hasGrid, children, ...rest }) => ( 7 | <div> 8 | <Navigation /> 9 | <ErrorList /> 10 | {hasGrid ? ( 11 | <Grid {...rest}> 12 | {children} 13 | </Grid> 14 | ) : children} 15 | </div> 16 | ); 17 | 18 | PageLayout.propTypes = { 19 | hasGrid: PropTypes.bool, 20 | }; 21 | 22 | PageLayout.defaultProps = { 23 | hasGrid: true, 24 | }; 25 | 26 | export default PageLayout; 27 | -------------------------------------------------------------------------------- /src/common/components/pages/HomePage/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageLayout from '../../layouts/PageLayout'; 3 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 4 | import './styles.scss'; 5 | import './styles.css'; 6 | 7 | const HomePage = (props) => ( 8 | <PageLayout className="rwd-background"> 9 | <PageHeader>Express-React-HMR-Boilerplate</PageHeader> 10 | <p className="red-border"> 11 | This is the demo site for project{' '} 12 | <a href="https://github.com/gocreating/express-react-hmr-boilerplate"> 13 | express-react-hmr-boilerplate 14 | </a> 15 | </p> 16 | </PageLayout> 17 | ); 18 | 19 | export default HomePage; 20 | -------------------------------------------------------------------------------- /src/common/components/pages/HomePage/sass-variables.scss: -------------------------------------------------------------------------------- 1 | $break-small: 320px; 2 | $break-large: 1200px; 3 | -------------------------------------------------------------------------------- /src/common/components/pages/HomePage/styles.css: -------------------------------------------------------------------------------- 1 | .red-border { 2 | border: 1px solid red; 3 | } 4 | -------------------------------------------------------------------------------- /src/common/components/pages/HomePage/styles.scss: -------------------------------------------------------------------------------- 1 | @import "./sass-variables"; 2 | 3 | .rwd-background { 4 | p { 5 | color: white; 6 | background-color: green; 7 | 8 | @media screen and (min-width: $break-small) { 9 | background-color: red; 10 | } 11 | @media screen and (min-width: $break-large) { 12 | background-color: black; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/common/components/pages/NotFoundPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 3 | import PageLayout from '../layouts/PageLayout'; 4 | 5 | const NotFoundPage = (props) => ( 6 | <PageLayout> 7 | <PageHeader>Page Not Found</PageHeader> 8 | <p>This is a 404 page.</p> 9 | </PageLayout> 10 | ); 11 | 12 | export default NotFoundPage; 13 | -------------------------------------------------------------------------------- /src/common/components/pages/admin/user/ListPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { push } from 'react-router-redux'; 4 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 5 | import Table from 'react-bootstrap/lib/Table'; 6 | import Resources from '../../../../constants/Resources'; 7 | import userAPI from '../../../../api/user'; 8 | import { pushErrors } from '../../../../actions/errorActions'; 9 | import { setCrrentPage } from '../../../../actions/pageActions'; 10 | import { setUsers } from '../../../../actions/userActions'; 11 | import PageLayout from '../../../layouts/AdminPageLayout'; 12 | import Pager from '../../../utils/BsPager'; 13 | 14 | class ListPage extends Component { 15 | constructor() { 16 | super(); 17 | this.handlePageChange = this._handlePageChange.bind(this); 18 | this.fetchUsers = this._fetchUsers.bind(this); 19 | } 20 | 21 | componentDidMount() { 22 | let { location } = this.props; 23 | 24 | this.fetchUsers(location.query.page || 1); 25 | } 26 | 27 | componentDidUpdate(prevProps) { 28 | let { page, users } = this.props; 29 | 30 | if (users.length === 0 && prevProps.page.current !== page.current) { 31 | this.fetchUsers(page.current); 32 | } 33 | } 34 | 35 | _handlePageChange(pageId) { 36 | let { dispatch } = this.props; 37 | 38 | dispatch(setCrrentPage(Resources.USER, pageId)); 39 | } 40 | 41 | _fetchUsers(page) { 42 | let { dispatch, apiEngine, location } = this.props; 43 | 44 | userAPI(apiEngine) 45 | .list({ page }) 46 | .catch((err) => { 47 | dispatch(pushErrors(err)); 48 | throw err; 49 | }) 50 | .then((json) => { 51 | dispatch(setUsers(json)); 52 | dispatch(push({ 53 | pathname: location.pathname, 54 | query: { page: json.page.current }, 55 | })); 56 | }); 57 | } 58 | 59 | render() { 60 | let { users, page } = this.props; 61 | 62 | return ( 63 | <PageLayout> 64 | <PageHeader>User List ({`${page.current} / ${page.total}`})</PageHeader> 65 | <Table striped bordered> 66 | <thead> 67 | <tr> 68 | <th>ID</th> 69 | <th>Avatar</th> 70 | <th>Name</th> 71 | <th>Email</th> 72 | <th>Role</th> 73 | <th>Created At</th> 74 | </tr> 75 | </thead> 76 | <tbody> 77 | {users.map((user) => ( 78 | <tr key={user._id}> 79 | <td>{user._id}</td> 80 | <td> 81 | <img 82 | src={user.avatarURL} 83 | style={{ 84 | maxHeight: 32, 85 | }} 86 | /> 87 | </td> 88 | <td>{user.name}</td> 89 | <td>{user.email.value}</td> 90 | <td>{user.role}</td> 91 | <td>{user.createdAt}</td> 92 | </tr> 93 | ))} 94 | </tbody> 95 | </Table> 96 | <Pager 97 | page={page} 98 | onPageChange={this.handlePageChange} 99 | /> 100 | </PageLayout> 101 | ); 102 | } 103 | } 104 | 105 | export default connect(({ apiEngine, pagination, entity }) => { 106 | let { page } = pagination.users; 107 | let userPages = pagination.users.pages[page.current] || { ids: [] }; 108 | let users = userPages.ids.map(id => entity.users[id]); 109 | 110 | return { 111 | apiEngine, 112 | users, 113 | page, 114 | }; 115 | })(ListPage); 116 | -------------------------------------------------------------------------------- /src/common/components/pages/demo/FormElementPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 3 | import PageLayout from '../../layouts/PageLayout'; 4 | import DemoForm from '../../forms/DemoForm'; 5 | 6 | let FormElementPage = (props) => ( 7 | <PageLayout> 8 | <PageHeader>Form Elements</PageHeader> 9 | <p> 10 | There are a rich amount of field types built inside the boilerplate. 11 | You can reuse them to prototype your custom form. 12 | </p> 13 | <DemoForm /> 14 | </PageLayout> 15 | ); 16 | 17 | export default FormElementPage; 18 | -------------------------------------------------------------------------------- /src/common/components/pages/user/EditPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 4 | import Row from 'react-bootstrap/lib/Row'; 5 | import Col from 'react-bootstrap/lib/Col'; 6 | import Button from 'react-bootstrap/lib/Button'; 7 | import PageLayout from '../../layouts/PageLayout'; 8 | import EditForm from '../../forms/user/EditForm'; 9 | import AvatarForm from '../../forms/user/AvatarForm'; 10 | import ChangePasswordForm from '../../forms/user/ChangePasswordForm'; 11 | 12 | let EditPage = () => { 13 | return ( 14 | <PageLayout> 15 | <Row> 16 | <Col md={12}> 17 | <Link to="/user/me"> 18 | <Button>Finish</Button> 19 | </Link> 20 | </Col> 21 | </Row> 22 | <hr /> 23 | <Row> 24 | <Col md={6}> 25 | <PageHeader>Edit Profile</PageHeader> 26 | <EditForm /> 27 | </Col> 28 | <Col md={6}> 29 | <PageHeader>Upload Avatar</PageHeader> 30 | <AvatarForm /> 31 | </Col> 32 | <Col md={6}> 33 | <PageHeader>Change Password</PageHeader> 34 | <ChangePasswordForm /> 35 | </Col> 36 | </Row> 37 | </PageLayout> 38 | ); 39 | }; 40 | 41 | export default EditPage; 42 | -------------------------------------------------------------------------------- /src/common/components/pages/user/ForgetPasswordPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 3 | import PageLayout from '../../layouts/PageLayout'; 4 | import ForgetPasswordForm from '../../forms/user/ForgetPasswordForm'; 5 | 6 | let ForgetPasswordPage = () => ( 7 | <PageLayout> 8 | <PageHeader>Forget Password</PageHeader> 9 | <ForgetPasswordForm /> 10 | </PageLayout> 11 | ); 12 | 13 | export default ForgetPasswordPage; 14 | -------------------------------------------------------------------------------- /src/common/components/pages/user/LoginPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 3 | import Alert from 'react-bootstrap/lib/Alert'; 4 | import Row from 'react-bootstrap/lib/Row'; 5 | import Col from 'react-bootstrap/lib/Col'; 6 | import PageLayout from '../../layouts/PageLayout'; 7 | import LoginForm from '../../forms/user/LoginForm'; 8 | import SocialAuthButtonList from '../../utils/SocialAuthButtonList'; 9 | 10 | let LoginPage = ({ location }) => ( 11 | <PageLayout> 12 | <PageHeader>Login</PageHeader> 13 | <Row> 14 | <Col md={12}> 15 | {location.query.next && ( 16 | <Alert bsStyle="warning"> 17 | <strong>Authentication Required</strong> 18 | {' '}Please login first. 19 | </Alert> 20 | )} 21 | </Col> 22 | </Row> 23 | <Row> 24 | <Col md={9}> 25 | <LoginForm /> 26 | </Col> 27 | <Col md={3}> 28 | <SocialAuthButtonList /> 29 | </Col> 30 | </Row> 31 | </PageLayout> 32 | ); 33 | 34 | export default LoginPage; 35 | -------------------------------------------------------------------------------- /src/common/components/pages/user/LogoutPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { push } from 'react-router-redux'; 4 | import userAPI from '../../../api/user'; 5 | import { logoutUser } from '../../../actions/userActions'; 6 | 7 | class LogoutPage extends React.Component { 8 | componentWillMount() { 9 | let { dispatch, apiEngine } = this.props; 10 | 11 | userAPI(apiEngine) 12 | .logout() 13 | .catch((err) => { 14 | alert('Logout user fail'); 15 | throw err; 16 | }) 17 | .then((json) => dispatch(logoutUser())) 18 | .then(() => dispatch(push('/'))); 19 | } 20 | 21 | render() { 22 | return null; 23 | } 24 | }; 25 | 26 | export default connect(state => ({ 27 | apiEngine: state.apiEngine, 28 | }))(LogoutPage); 29 | -------------------------------------------------------------------------------- /src/common/components/pages/user/RegisterPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 3 | import Row from 'react-bootstrap/lib/Row'; 4 | import Col from 'react-bootstrap/lib/Col'; 5 | import PageLayout from '../../layouts/PageLayout'; 6 | import RegisterForm from '../../forms/user/RegisterForm'; 7 | import SocialAuthButtonList from '../../utils/SocialAuthButtonList'; 8 | 9 | const RegisterPage = (props) => ( 10 | <PageLayout> 11 | <PageHeader>Register</PageHeader> 12 | <Row> 13 | <Col md={9}> 14 | <RegisterForm /> 15 | </Col> 16 | <Col md={3}> 17 | <SocialAuthButtonList /> 18 | </Col> 19 | </Row> 20 | </PageLayout> 21 | ); 22 | 23 | export default RegisterPage; 24 | -------------------------------------------------------------------------------- /src/common/components/pages/user/ResetPasswordPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PageHeader from 'react-bootstrap/lib/PageHeader'; 3 | import PageLayout from '../../layouts/PageLayout'; 4 | import ResetPasswordForm from '../../forms/user/ResetPasswordForm'; 5 | 6 | let ResetPasswordPage = (props) => ( 7 | <PageLayout> 8 | <PageHeader>Reset Password</PageHeader> 9 | <ResetPasswordForm /> 10 | </PageLayout> 11 | ); 12 | 13 | export default ResetPasswordPage; 14 | -------------------------------------------------------------------------------- /src/common/components/pages/user/VerifyEmailPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Link } from 'react-router'; 4 | import Alert from 'react-bootstrap/lib/Alert'; 5 | import userAPI from '../../../api/user'; 6 | import { pushErrors } from '../../../actions/errorActions'; 7 | import PageLayout from '../../layouts/PageLayout'; 8 | 9 | class VerificationPage extends React.Component { 10 | constructor() { 11 | super(); 12 | this.state = { 13 | isVerifying: true, 14 | isFail: true, 15 | }; 16 | } 17 | 18 | componentWillMount() { 19 | let { dispatch, apiEngine, location } = this.props; 20 | if (process.env.BROWSER) { 21 | userAPI(apiEngine) 22 | .verifyEmail({ token: location.query.token }) 23 | .catch((err) => { 24 | this.setState({ 25 | isVerifying: false, 26 | isFail: true, 27 | }); 28 | dispatch(pushErrors(err)); 29 | throw err; 30 | }) 31 | .then((json) => { 32 | this.setState({ 33 | isVerifying: false, 34 | isFail: false, 35 | }); 36 | }); 37 | } 38 | } 39 | 40 | render() { 41 | let { isVerifying, isFail } = this.state; 42 | 43 | if (isVerifying) { 44 | return ( 45 | <PageLayout> 46 | <p>Please wait for a while...</p> 47 | </PageLayout> 48 | ); 49 | } 50 | 51 | if (isFail) { 52 | return ( 53 | <PageLayout> 54 | <Alert bsStyle="danger"> 55 | <strong>Verification Failed</strong> 56 | </Alert> 57 | </PageLayout> 58 | ); 59 | } 60 | 61 | return ( 62 | <PageLayout> 63 | <Alert bsStyle="success"> 64 | <strong>Verification Success</strong> 65 | <p> 66 | Go to <Link to="/user/login">Login Page</Link> 67 | </p> 68 | </Alert> 69 | </PageLayout> 70 | ); 71 | } 72 | }; 73 | 74 | export default connect(state => ({ 75 | apiEngine: state.apiEngine, 76 | }))(VerificationPage); 77 | -------------------------------------------------------------------------------- /src/common/components/utils/BsNavbar.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | class BsNavbar extends Component { 5 | componentDidMount() { 6 | if (process.env.BROWSER && this.props.fixedTop) { 7 | document.body.style.marginTop = '50px'; 8 | } 9 | } 10 | 11 | render() { 12 | const { 13 | fixedTop, 14 | staticTop, 15 | children, 16 | ...rest 17 | } = this.props; 18 | const cx = classNames( 19 | 'navbar', 20 | 'navbar-default', 21 | { 22 | 'navbar-fixed-top': fixedTop, 23 | 'navbar-static-top': staticTop, 24 | } 25 | ); 26 | return ( 27 | <nav className={cx} {...rest}> 28 | {children} 29 | </nav> 30 | ); 31 | } 32 | }; 33 | 34 | const Header = ({ children, ...rest }) => ( 35 | <div className="navbar-header" {...rest}> 36 | <button 37 | type="button" 38 | className="navbar-toggle collapsed" 39 | data-toggle="collapse" 40 | data-target="#navbar" 41 | aria-expanded="false"> 42 | <span className="sr-only">Toggle navigation</span> 43 | <span className="icon-bar"></span> 44 | <span className="icon-bar"></span> 45 | <span className="icon-bar"></span> 46 | </button> 47 | {children} 48 | </div> 49 | ); 50 | 51 | const Body = ({ children, ...rest }) => ( 52 | <div className="collapse navbar-collapse" id="navbar" {...rest}> 53 | {children} 54 | </div> 55 | ); 56 | 57 | const Nav = ({ right, children, ...rest }) => { 58 | const cx = classNames( 59 | 'nav', 60 | 'navbar-nav', 61 | { 62 | 'navbar-right': right, 63 | } 64 | ); 65 | return ( 66 | <ul className={cx} {...rest}> 67 | {children} 68 | </ul> 69 | ); 70 | }; 71 | 72 | const Dropdown = ({ title, children }) => ( 73 | <li className="dropdown"> 74 | <a 75 | href="#" 76 | className="dropdown-toggle" 77 | data-toggle="dropdown" 78 | role="button" 79 | aria-haspopup="true" 80 | aria-expanded="false" 81 | > 82 | {title} 83 | <span className="caret"></span> 84 | </a> 85 | <ul className="dropdown-menu"> 86 | {children} 87 | </ul> 88 | </li> 89 | ); 90 | 91 | BsNavbar.propTypes = { 92 | fixedTop: PropTypes.bool, 93 | staticTop: PropTypes.bool, 94 | }; 95 | 96 | Nav.propTypes = { 97 | right: PropTypes.bool, 98 | }; 99 | 100 | Dropdown.propTypes = { 101 | title: PropTypes.oneOfType([ 102 | PropTypes.string, 103 | PropTypes.element, 104 | ]), 105 | }; 106 | 107 | BsNavbar.Header = Header; 108 | BsNavbar.Body = Body; 109 | BsNavbar.Nav = Nav; 110 | BsNavbar.Dropdown = Dropdown; 111 | 112 | export default BsNavbar; 113 | -------------------------------------------------------------------------------- /src/common/components/utils/BsPager.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes, Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import cx from 'classnames'; 4 | import Text from '../widgets/Text'; 5 | 6 | let style = { 7 | cursor: 'pointer', 8 | margin: 2, 9 | }; 10 | 11 | let handlePagerButtonClick = (props) => { 12 | let { disabled, onClick } = props; 13 | 14 | if (disabled) { 15 | return undefined; 16 | } else { 17 | return onClick; 18 | } 19 | }; 20 | 21 | let PagerButton = (props) => { 22 | /* eslint-disable */ 23 | // consume prop `onClick` 24 | let { disabled, onClick, children, ...rest } = props; 25 | /* eslint-enable */ 26 | 27 | return ( 28 | <li 29 | className={cx({ 'disabled': disabled })} 30 | style={style} 31 | {...rest} 32 | > 33 | <a onClick={handlePagerButtonClick(props)}> 34 | {children} 35 | </a> 36 | </li> 37 | ); 38 | }; 39 | 40 | class Pager extends Component { 41 | _handlePageChange(pageId) { 42 | let { onPageChange } = this.props; 43 | 44 | if (onPageChange) { 45 | onPageChange(pageId); 46 | } 47 | } 48 | 49 | render() { 50 | let { simple, page } = this.props; 51 | let pageStatus = { 52 | isFirst: page.current === page.first, 53 | isLast: page.current === page.last, 54 | }; 55 | 56 | return ( 57 | <nav> 58 | <ul className="pager"> 59 | {!simple && ( 60 | <PagerButton 61 | disabled={pageStatus.isFirst} 62 | onClick={this._handlePageChange.bind(this, page.first)} 63 | > 64 | <i className="fa fa-angle-double-left" aria-hidden="true" /> 65 | {' '}<Text id="page.first" /> 66 | </PagerButton> 67 | )} 68 | 69 | <PagerButton 70 | disabled={pageStatus.isFirst} 71 | onClick={this._handlePageChange.bind(this, page.current - 1)} 72 | > 73 | <i className="fa fa-chevron-left" aria-hidden="true" /> 74 | {' '}<Text id="page.prev" /> 75 | </PagerButton> 76 | 77 | <PagerButton 78 | disabled={pageStatus.isLast} 79 | onClick={this._handlePageChange.bind(this, page.current + 1)} 80 | > 81 | <Text id="page.next" />{' '} 82 | <i className="fa fa-chevron-right" aria-hidden="true" /> 83 | </PagerButton> 84 | 85 | {!simple && ( 86 | <PagerButton 87 | disabled={pageStatus.isLast} 88 | onClick={this._handlePageChange.bind(this, page.last)} 89 | > 90 | <Text id="page.last" />{' '} 91 | <i className="fa fa-angle-double-right" aria-hidden="true" /> 92 | </PagerButton> 93 | )} 94 | </ul> 95 | </nav> 96 | ); 97 | } 98 | } 99 | 100 | Pager.propTypes = { 101 | simple: PropTypes.bool, 102 | page: PropTypes.object.isRequired, 103 | handlePageChange: PropTypes.func, 104 | }; 105 | 106 | export default connect()(Pager); 107 | -------------------------------------------------------------------------------- /src/common/components/utils/ErrorList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { push } from 'react-router-redux'; 3 | import isString from 'lodash/isString'; 4 | import { connect } from 'react-redux'; 5 | import Grid from 'react-bootstrap/lib/Grid'; 6 | import Alert from 'react-bootstrap/lib/Alert'; 7 | import Table from 'react-bootstrap/lib/Table'; 8 | import { removeError } from '../../actions/errorActions'; 9 | import ErrorCodes from '../../constants/ErrorCodes'; 10 | 11 | function renderMetaContent(metaContent) { 12 | if (isString(metaContent)) { 13 | return metaContent; 14 | } 15 | 16 | return ( 17 | <pre> 18 | {JSON.stringify(metaContent, null, 2)} 19 | </pre> 20 | ); 21 | } 22 | 23 | function renderMeta(meta) { 24 | if (isString(meta)) { 25 | return ( 26 | <p> 27 | {meta} 28 | </p> 29 | ); 30 | } 31 | 32 | return ( 33 | <Table 34 | condensed 35 | responsive 36 | style={{ 37 | marginBottom: 0, 38 | background: 'white', 39 | }} 40 | > 41 | <tbody> 42 | {Object.keys(meta).map((key) => ( 43 | <tr key={key}> 44 | <td>{key}</td> 45 | <td> 46 | {renderMetaContent(meta[key])} 47 | </td> 48 | </tr> 49 | ))} 50 | </tbody> 51 | </Table> 52 | ); 53 | } 54 | 55 | let ErrorList = ({ errors, dispatch }) => ( 56 | <Grid> 57 | {errors.map((error) => { 58 | if ([ 59 | ErrorCodes.TOKEN_EXPIRATION, 60 | ErrorCodes.BAD_TOKEN, 61 | ].indexOf(error.code) >= 0) { 62 | dispatch(removeError(error.id)); 63 | dispatch(push('/user/login')); 64 | } 65 | if ([ 66 | ErrorCodes.USER_UNAUTHORIZED, 67 | ErrorCodes.PERMISSION_DENIED, 68 | ].indexOf(error.code) >= 0) { 69 | dispatch(removeError(error.id)); 70 | dispatch(push('/')); 71 | } 72 | 73 | return ( 74 | <Alert 75 | key={error.id} 76 | bsStyle="danger" 77 | onDismiss={() => dispatch(removeError(error.id))} 78 | > 79 | <h4>{error.title}</h4> 80 | {' ' + error.detail} 81 | {error.meta && renderMeta(error.meta)} 82 | </Alert> 83 | ); 84 | })} 85 | </Grid> 86 | ); 87 | 88 | export default connect(state => ({ 89 | errors: state.errors, 90 | }))(ErrorList); 91 | -------------------------------------------------------------------------------- /src/common/components/utils/LocaleProvider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { IntlProvider } from 'react-intl'; 4 | import { updateLocale } from '../../actions/intlActions'; 5 | 6 | class LocaleProvider extends Component { 7 | componentDidMount() { 8 | let { dispatch, intl } = this.props; 9 | let lang = intl.locale || navigator.language; 10 | 11 | dispatch(updateLocale(lang)) 12 | .then(() => { 13 | console.log('load locale (automatically) ok'); 14 | }, (err) => { 15 | alert('load locale (automatically) fail', err); 16 | }); 17 | } 18 | 19 | render() { 20 | const { intl, children } = this.props; 21 | 22 | return ( 23 | <IntlProvider locale={intl.locale} messages={intl.messages}> 24 | {children} 25 | </IntlProvider> 26 | ); 27 | } 28 | }; 29 | 30 | export default connect((state) => ({ 31 | intl: state.intl, 32 | }))(LocaleProvider); 33 | -------------------------------------------------------------------------------- /src/common/components/utils/MenuItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | const MenuItem = ({ title, href, ...rest }) => ( 4 | <li> 5 | <a href={href} {...rest}> 6 | {title} 7 | </a> 8 | </li> 9 | ); 10 | 11 | MenuItem.propTypes = { 12 | title: PropTypes.string, 13 | href: PropTypes.string, 14 | }; 15 | 16 | MenuItem.defaultProps = { 17 | href: '#', 18 | }; 19 | 20 | export default MenuItem; 21 | -------------------------------------------------------------------------------- /src/common/components/utils/NavLink.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import activeComponent from 'react-router-active-component'; 3 | 4 | const NavLink = ({ children, ...rest }) => { 5 | const ActiveLink = activeComponent('li'); 6 | return ( 7 | <ActiveLink {...rest}> 8 | {children} 9 | </ActiveLink> 10 | ); 11 | }; 12 | 13 | export default NavLink; 14 | -------------------------------------------------------------------------------- /src/common/components/utils/RangeSlider.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import ReactSlider from 'react-slider'; 3 | 4 | let defaultValue = { 5 | min: 0, 6 | max: 0, 7 | }; 8 | 9 | class RangeSlider extends Component { 10 | render() { 11 | let { 12 | input, 13 | ...rest 14 | } = this.props; 15 | 16 | return ( 17 | <ReactSlider 18 | {...rest} 19 | onChange={(value) => { 20 | input.onChange({ 21 | min: value[0], 22 | max: value[1], 23 | }); 24 | }} 25 | value={[ 26 | input.value.min || defaultValue.min, 27 | input.value.max || defaultValue.max, 28 | ]} 29 | withBars 30 | > 31 | <div className="slider-handle"></div> 32 | <div className="slider-handle"></div> 33 | </ReactSlider> 34 | ); 35 | } 36 | } 37 | 38 | RangeSlider.propTypes = { 39 | input: PropTypes.object.isRequired, 40 | }; 41 | 42 | export default RangeSlider; 43 | -------------------------------------------------------------------------------- /src/common/components/utils/SocialAuthButtonList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import Head from '../widgets/Head'; 4 | 5 | let SocialAuthButtonList = ({ routing }) => { 6 | let { next } = routing.locationBeforeTransitions.query; 7 | let search = next ? '?next=' + next : ''; 8 | 9 | return ( 10 | <div> 11 | <Head 12 | links={[ 13 | 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/5.0.0/bootstrap-social.min.css', 14 | ]} 15 | /> 16 | <a 17 | href={`/auth/facebook${search}`} 18 | className="btn btn-block btn-social btn-facebook" 19 | > 20 | <span className="fa fa-facebook"></span>Login with Facebook 21 | </a> 22 | <a 23 | href={`/auth/linkedin${search}`} 24 | className="btn btn-block btn-social btn-linkedin" 25 | > 26 | <span className="fa fa-linkedin"></span>Login with LinkedIn 27 | </a> 28 | </div> 29 | ); 30 | }; 31 | 32 | export default connect(state => ({ 33 | routing: state.routing, 34 | }))(SocialAuthButtonList); 35 | -------------------------------------------------------------------------------- /src/common/components/widgets/Head.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | const Head = ({ title, metas, links, scripts }) => ( 5 | <Helmet 6 | title={title} 7 | meta={metas} 8 | link={links.map(src => 9 | ({ href: src, rel: 'stylesheet' }))} 10 | script={scripts.map(src => 11 | ({ src: src, type: 'text/javascript' }))} /> 12 | ); 13 | 14 | Head.defaultProps = { 15 | metas: [], 16 | links: [], 17 | scripts: [], 18 | }; 19 | 20 | Head.propTypes = { 21 | title: PropTypes.string, 22 | metas: PropTypes.arrayOf(PropTypes.object), 23 | links: PropTypes.arrayOf(PropTypes.string), 24 | scripts: PropTypes.arrayOf(PropTypes.string), 25 | }; 26 | 27 | export default Head; 28 | -------------------------------------------------------------------------------- /src/common/components/widgets/Text.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | const Text = (props) => ( 5 | <FormattedMessage {...props} /> 6 | ); 7 | 8 | export default Text; 9 | -------------------------------------------------------------------------------- /src/common/components/widgets/Time.js: -------------------------------------------------------------------------------- 1 | // inspired by <https://github.com/andreypopp/react-time> 2 | import React, { PropTypes } from 'react'; 3 | import moment from 'moment'; 4 | 5 | const Time = ({ value, format, relative, ...rest }) => { 6 | let v = null; 7 | if (value) { 8 | v = moment(value); 9 | v = relative ? v.fromNow() : v.format(format); 10 | } 11 | return ( 12 | <time {...rest}>{v}</time> 13 | ); 14 | }; 15 | 16 | Time.defaultProps = { 17 | format: 'YYYY-MM-DD HH:mm:ss', 18 | }; 19 | 20 | Time.propTypes = { 21 | value: PropTypes.oneOfType([ 22 | PropTypes.instanceOf(Date), 23 | PropTypes.string, 24 | ]), 25 | format: PropTypes.string, 26 | relative: PropTypes.bool, 27 | }; 28 | 29 | export default Time; 30 | -------------------------------------------------------------------------------- /src/common/constants/ActionTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_COOKIE: 'SET_COOKIE', 3 | 4 | SET_API_ENGINE: 'SET_API_ENGINE', 5 | 6 | LOGIN_USER: 'LOGIN_USER', 7 | LOGOUT_USER: 'LOGOUT_USER', 8 | 9 | UPDATE_LOCALE: 'UPDATE_LOCALE', 10 | 11 | PUSH_ERRORS: 'PUSH_ERRORS', 12 | REMOVE_ERROR: 'REMOVE_ERROR', 13 | 14 | SET_ENTITIES: 'SET_ENTITIES', 15 | SET_PAGES: 'SET_PAGES', 16 | SET_CURRENT_PAGE: 'SET_CURRENT_PAGE', 17 | PREPEND_ENTITIES_INTO_PAGE: 'PREPEND_ENTITIES_INTO_PAGE', 18 | APPEND_ENTITIES_INTO_PAGE: 'PUSH_ENTITIES_INTO_PAGE', 19 | REMOVE_ENTITIES_FROM_PAGE: 'REMOVE_ENTITIES_FROM_PAGE', 20 | }; 21 | -------------------------------------------------------------------------------- /src/common/constants/ErrorCodes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UNKNOWN_EXCEPTION: 'UNKNOWN_EXCEPTION', 3 | ODM_OPERATION_FAIL: 'ODM_OPERATION_FAIL', 4 | STATE_PRE_FETCHING_FAIL: 'STATE_PRE_FETCHING_FAIL', 5 | USER_UNAUTHORIZED: 'USER_UNAUTHORIZED', 6 | USER_EXISTED: 'USER_EXISTED', 7 | TOKEN_REUSED: 'TOKEN_REUSED', 8 | BAD_TOKEN: 'BAD_TOKEN', 9 | TOKEN_EXPIRATION: 'TOKEN_EXPIRATION', 10 | PERMISSION_DENIED: 'PERMISSION_DENIED', 11 | LOCALE_NOT_SUPPORTED: 'LOCALE_NOT_SUPPORTED', 12 | ODM_VALIDATION: 'ODM_VALIDATION', 13 | INVALID_RECAPTCHA: 'INVALID_RECAPTCHA', 14 | INVALID_DATA: 'INVALID_DATA', 15 | AUTHORIZATION_FAIL: 'AUTHORIZATION_FAIL', 16 | SEND_EMAIL_FAIL: 'SEND_EMAIL_FAIL', 17 | }; 18 | -------------------------------------------------------------------------------- /src/common/constants/Errors.js: -------------------------------------------------------------------------------- 1 | import ErrorCodes from './ErrorCodes'; 2 | 3 | export default { 4 | [ErrorCodes.UNKNOWN_EXCEPTION]: { 5 | code: ErrorCodes.UNKNOWN_EXCEPTION, 6 | status: 500, 7 | title: 'Unknown Exception', 8 | detail: 'Something wrong happened.', 9 | }, 10 | [ErrorCodes.ODM_OPERATION_FAIL]: { 11 | code: ErrorCodes.ODM_OPERATION_FAIL, 12 | status: 500, 13 | title: 'Database Operation Failed', 14 | detail: 'Current database operation is invalid.', 15 | }, 16 | [ErrorCodes.ODM_VALIDATION]: { 17 | code: ErrorCodes.ODM_VALIDATION, 18 | status: 400, 19 | title: 'Database Validation Failed', 20 | detail: 'The data is invalid.', 21 | }, 22 | [ErrorCodes.INVALID_RECAPTCHA]: { 23 | code: ErrorCodes.INVALID_RECAPTCHA, 24 | status: 400, 25 | title: 'Invalid Recaptcha', 26 | detail: 'The value of recaptcha is invalid.', 27 | }, 28 | [ErrorCodes.INVALID_DATA]: { 29 | code: ErrorCodes.INVALID_DATA, 30 | status: 400, 31 | title: 'Invalid Data', 32 | detail: 'You are sending invalid data.', 33 | }, 34 | [ErrorCodes.STATE_PRE_FETCHING_FAIL]: { 35 | code: ErrorCodes.STATE_PRE_FETCHING_FAIL, 36 | status: 500, 37 | title: 'Server-Side State Fetching Failed', 38 | detail: 'Fail to pre-fetch state on server side.', 39 | }, 40 | [ErrorCodes.USER_UNAUTHORIZED]: { 41 | code: ErrorCodes.USER_UNAUTHORIZED, 42 | status: 401, 43 | title: 'User Unauthorized', 44 | detail: 45 | 'You are a guest or invalid user. Please login to access the resource.', 46 | }, 47 | [ErrorCodes.USER_EXISTED]: { 48 | code: ErrorCodes.USER_EXISTED, 49 | status: 400, 50 | title: 'User Existed', 51 | detail: 'This user is already registered.', 52 | }, 53 | [ErrorCodes.TOKEN_REUSED]: { 54 | code: ErrorCodes.TOKEN_REUSED, 55 | status: 400, 56 | title: 'Token Reused', 57 | detail: 'The token is reused.', 58 | }, 59 | [ErrorCodes.BAD_TOKEN]: { 60 | code: ErrorCodes.BAD_TOKEN, 61 | status: 400, 62 | title: 'Bad Token', 63 | detail: 'Fail to decode the token.', 64 | }, 65 | [ErrorCodes.TOKEN_EXPIRATION]: { 66 | code: ErrorCodes.TOKEN_EXPIRATION, 67 | status: 401, 68 | title: 'Token Expired', 69 | detail: 'Your token has expired.', 70 | }, 71 | [ErrorCodes.PERMISSION_DENIED]: { 72 | code: ErrorCodes.PERMISSION_DENIED, 73 | status: 403, 74 | title: 'Permission Denied', 75 | detail: 'You are not allowed to access the resource.', 76 | }, 77 | [ErrorCodes.LOCALE_NOT_SUPPORTED]: { 78 | code: ErrorCodes.LOCALE_NOT_SUPPORTED, 79 | status: 400, 80 | title: 'Locale not supported', 81 | detail: 'We don\'t support this locale.', 82 | }, 83 | [ErrorCodes.AUTHORIZATION_FAIL]: { 84 | code: ErrorCodes.AUTHORIZATION_FAIL, 85 | status: 400, 86 | title: 'Authorization Failed', 87 | detail: 'Please make sure you authorize all required information to us.', 88 | }, 89 | [ErrorCodes.SEND_EMAIL_FAIL]: { 90 | code: ErrorCodes.SEND_EMAIL_FAIL, 91 | status: 500, 92 | title: 'Email Not Sent', 93 | detail: 'Mail service didn\'t send the mail correctly.', 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /src/common/constants/FormNames.js: -------------------------------------------------------------------------------- 1 | export default { 2 | DEMO: 'DEMO', 3 | USER_REGISTER: 'USER_REGISTER', 4 | USER_LOGIN: 'USER_LOGIN', 5 | USER_EDIT: 'USER_EDIT', 6 | USER_AVATAR: 'USER_AVATAR', 7 | USER_VERIFY_EMAIL: 'USER_VERIFY_EMAIL', 8 | USER_CHANGE_PASSWORD: 'USER_CHANGE_PASSWORD', 9 | USER_FORGET_PASSWORD: 'USER_FORGET_PASSWORD', 10 | USER_RESET_PASSWORD: 'USER_RESET_PASSWORD', 11 | }; 12 | -------------------------------------------------------------------------------- /src/common/constants/Resources.js: -------------------------------------------------------------------------------- 1 | export default { 2 | USER: 'USER', 3 | TODO: 'TODO', 4 | }; 5 | -------------------------------------------------------------------------------- /src/common/constants/Roles.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // end user 3 | USER: 'USER', 4 | // system administrator 5 | ADMIN: 'ADMIN', 6 | // root 7 | ROOT: 'ROOT', 8 | }; 9 | -------------------------------------------------------------------------------- /src/common/i18n/en-us.js: -------------------------------------------------------------------------------- 1 | export default { 2 | parentLocale: 'en', 3 | 'nav.home': 'Home', 4 | 'nav.todo': 'Todo', 5 | 'nav.language': 'Language', 6 | 'nav.user': 'User', 7 | 'nav.user.login': 'Login', 8 | 'nav.user.register': 'Register', 9 | 'nav.user.profile': 'Profile', 10 | 'nav.user.logout': 'Logout', 11 | 'page.first': 'First Page', 12 | 'page.prev': 'Previous', 13 | 'page.next': 'Next', 14 | 'page.last': 'Last Page', 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/i18n/zh-tw.js: -------------------------------------------------------------------------------- 1 | export default { 2 | parentLocale: 'zh', 3 | 'nav.home': '首頁', 4 | 'nav.todo': '待辦', 5 | 'nav.language': '語言', 6 | 'nav.user': '會員', 7 | 'nav.user.login': '登入', 8 | 'nav.user.register': '註冊', 9 | 'nav.user.profile': '會員中心', 10 | 'nav.user.logout': '登出', 11 | 'page.first': '第一頁', 12 | 'page.prev': '上一頁', 13 | 'page.next': '下一頁', 14 | 'page.last': '最後一頁', 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/reducers/apiEngineReducer.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | 3 | let initState = null; 4 | 5 | export default (state = initState, action) => { 6 | switch (action.type) { 7 | case ActionTypes.SET_API_ENGINE: { 8 | return action.apiEngine; 9 | } 10 | default: { 11 | return state; 12 | } 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/common/reducers/cookieReducer.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | import ActionTypes from '../constants/ActionTypes'; 3 | import deserializeCookieMap from '../utils/deserializeCookieMap'; 4 | 5 | let initCookies = {}; 6 | 7 | if (process.env.BROWSER) { 8 | initCookies = deserializeCookieMap(cookie.parse(document.cookie)); 9 | } else { 10 | initCookies = {}; 11 | } 12 | 13 | export default (state = initCookies, action) => { 14 | switch (action.type) { 15 | case ActionTypes.SET_COOKIE: { 16 | return { 17 | ...state, 18 | [action.cookie.name]: action.cookie.value, 19 | }; 20 | } 21 | default: { 22 | return state; 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/common/reducers/entityReducer.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | import merge from 'lodash/merge'; 3 | 4 | let initState = {}; 5 | 6 | export default (state = initState, action) => { 7 | switch (action.type) { 8 | case ActionTypes.SET_ENTITIES: { 9 | return merge({}, state, action.normalized.entities); 10 | } 11 | default: { 12 | return state; 13 | } 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/reducers/errorReducer.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | 3 | let initState = []; 4 | 5 | export default (state = initState, action) => { 6 | if (!action.errors) { 7 | action.errors = []; 8 | } 9 | switch (action.type) { 10 | case ActionTypes.PUSH_ERRORS: { 11 | return [ 12 | ...state, 13 | ...action.errors.map((error) => ({ 14 | id: Math.random(), 15 | ...error, 16 | })), 17 | ]; 18 | } 19 | case ActionTypes.REMOVE_ERROR: { 20 | return [ 21 | ...state.filter(error => error.id !== action.id), 22 | ]; 23 | } 24 | default: { 25 | return state; 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/common/reducers/formReducer.js: -------------------------------------------------------------------------------- 1 | import { reducer as formReducer } from 'redux-form'; 2 | export default formReducer; 3 | -------------------------------------------------------------------------------- /src/common/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import routing from './routerReducer'; 3 | import cookies from './cookieReducer'; 4 | import errors from './errorReducer'; 5 | import apiEngine from './apiEngineReducer'; 6 | import form from './formReducer'; 7 | import intl from './intlReducer'; 8 | import entity from './entityReducer'; 9 | import pagination from './paginationReducer'; 10 | 11 | const rootReducer = combineReducers({ 12 | routing, 13 | cookies, 14 | errors, 15 | apiEngine, 16 | form, // must mount as `form` from redux-form's docs 17 | intl, 18 | entity, 19 | pagination, 20 | }); 21 | 22 | export default rootReducer; 23 | -------------------------------------------------------------------------------- /src/common/reducers/intlReducer.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | import messages from '../i18n/en-us'; 3 | 4 | const initLocale = { 5 | locale: 'en-us', 6 | messages: messages, 7 | }; 8 | 9 | export default (state = initLocale, action) => { 10 | switch (action.type) { 11 | case ActionTypes.UPDATE_LOCALE: { 12 | return { 13 | locale: action.locale, 14 | messages: action.messages, 15 | }; 16 | } 17 | default: { 18 | return state; 19 | } 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /src/common/reducers/routerReducer.js: -------------------------------------------------------------------------------- 1 | import { routerReducer } from 'react-router-redux'; 2 | export default routerReducer; 3 | -------------------------------------------------------------------------------- /src/common/reducers/todoReducer.js: -------------------------------------------------------------------------------- 1 | import ActionTypes from '../constants/ActionTypes'; 2 | 3 | export default (state = [], action) => { 4 | switch (action.type) { 5 | case ActionTypes.SET_TODO: { 6 | return [ 7 | ...action.todos, 8 | ]; 9 | } 10 | case ActionTypes.ADD_TODO: { 11 | return [ 12 | action.todo, 13 | ...state, 14 | ]; 15 | } 16 | case ActionTypes.REMOVE_TODO: { 17 | return [ 18 | ...state.filter(todo => todo._id !== action.id), 19 | ]; 20 | } 21 | default: { 22 | return state; 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/common/routes/admin/index.js: -------------------------------------------------------------------------------- 1 | import Roles from '../../constants/Roles'; 2 | import compose from '../../utils/composeEnterHooks'; 3 | 4 | export default (store) => ({ 5 | path: 'admin', 6 | getIndexRoute(location, cb) { 7 | require.ensure([], (require) => { 8 | cb(null, { 9 | component: 10 | require('../../components/pages/admin/user/ListPage').default, 11 | }); 12 | }); 13 | }, 14 | getChildRoutes(location, cb) { 15 | require.ensure([], (require) => { 16 | cb(null, [ 17 | require('./user').default(store), 18 | ]); 19 | }); 20 | }, 21 | onEnter: compose.series( 22 | require('../../utils/authRequired').default(store), 23 | require('../../utils/roleRequired').default(store)(Roles.ADMIN), 24 | ), 25 | }); 26 | -------------------------------------------------------------------------------- /src/common/routes/admin/user/index.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'user', 3 | getIndexRoute(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, { 6 | component: 7 | require('../../../components/pages/admin/user/ListPage').default, 8 | }); 9 | }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/common/routes/demo/formElement.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'form-element', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/demo/FormElementPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/routes/demo/index.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'demo', 3 | getChildRoutes(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, [ 6 | require('./formElement').default(store), 7 | ]); 8 | }); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /src/common/routes/index.js: -------------------------------------------------------------------------------- 1 | // essential polyfill for `require.ensure` 2 | import '../utils/ensure-polyfill'; 3 | import AppLayout from '../components/layouts/AppLayout'; 4 | 5 | export default (store) => ({ 6 | path: '/', 7 | component: AppLayout, 8 | getChildRoutes(location, cb) { 9 | require.ensure([], (require) => { 10 | cb(null, [ 11 | require('./admin').default(store), 12 | require('./user').default(store), 13 | require('./todo').default(store), 14 | require('./demo').default(store), 15 | require('./notFound').default(store), 16 | ]); 17 | }); 18 | }, 19 | getIndexRoute(location, cb) { 20 | require.ensure([], (require) => { 21 | cb(null, { 22 | component: require('../components/pages/HomePage').default, 23 | }); 24 | }); 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /src/common/routes/notFound.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: '*', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../components/pages/NotFoundPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/routes/todo.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'todo', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../components/pages/todo/ListPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/routes/user/edit.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'me/edit', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/user/EditPage').default); 6 | }); 7 | }, 8 | onEnter: require('../../utils/authRequired').default(store), 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/routes/user/forgetPassword.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'password/forget', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb( 6 | null, 7 | require('../../components/pages/user/ForgetPasswordPage').default 8 | ); 9 | }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/common/routes/user/index.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'user', 3 | getChildRoutes(location, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, [ 6 | require('./register').default(store), 7 | require('./verifyEmail').default(store), 8 | require('./login').default(store), 9 | require('./edit').default(store), 10 | require('./forgetPassword').default(store), 11 | require('./resetPassword').default(store), 12 | require('./logout').default(store), 13 | require('./me').default(store), 14 | ]); 15 | }); 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /src/common/routes/user/login.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'login', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/user/LoginPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/routes/user/logout.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'logout', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/user/LogoutPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/routes/user/me.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'me', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/user/ShowSelfPage').default); 6 | }); 7 | }, 8 | onEnter: require('../../utils/authRequired').default(store), 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/routes/user/register.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'register', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/user/RegisterPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/routes/user/resetPassword.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'password/reset', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb( 6 | null, 7 | require('../../components/pages/user/ResetPasswordPage').default 8 | ); 9 | }); 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /src/common/routes/user/verifyEmail.js: -------------------------------------------------------------------------------- 1 | export default (store) => ({ 2 | path: 'email/verify', 3 | getComponent(nextState, cb) { 4 | require.ensure([], (require) => { 5 | cb(null, require('../../components/pages/user/VerifyEmailPage').default); 6 | }); 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /src/common/schemas/index.js: -------------------------------------------------------------------------------- 1 | import { Schema } from 'normalizr'; 2 | 3 | export let todoSchema = new Schema('todos', { 4 | idAttribute: '_id', 5 | }); 6 | 7 | export let userSchema = new Schema('users', { 8 | idAttribute: '_id', 9 | }); 10 | -------------------------------------------------------------------------------- /src/common/utils/ApiEngine.js: -------------------------------------------------------------------------------- 1 | // ref: <https://github.com/erikras/react-redux-universal-hot-example/blob/master/src/helpers/ApiClient.js> 2 | import superagent from 'superagent'; 3 | import getPort from '../../server/utils/getPort'; 4 | 5 | const BASE = process.env.BROWSER ? '' : `http://localhost:${getPort()}`; 6 | const methods = ['get', 'post', 'put', 'patch', 'del']; 7 | 8 | function formatUrl(path) { 9 | return `${BASE}${path}`; 10 | } 11 | 12 | export default class ApiEngine { 13 | constructor(req) { 14 | methods.forEach((method) => { 15 | this[method] = (path, { params, data, files } = {}) => { 16 | return new Promise((resolve, reject) => { 17 | const request = superagent[method](formatUrl(path)); 18 | 19 | if (params) { 20 | request.query(params); 21 | } 22 | 23 | if (!process.env.BROWSER && req.get('cookie')) { 24 | request.set('cookie', req.get('cookie')); 25 | } 26 | 27 | if (data) { 28 | request.send(data); 29 | } 30 | 31 | if (files) { 32 | let formData = new FormData(); 33 | Object.keys(files).forEach((name) => { 34 | formData.append(name, files[name]); 35 | }); 36 | request.send(formData); 37 | } 38 | 39 | request.end((err, { body } = {}) => { 40 | if (err) { 41 | return reject(body || err); 42 | } 43 | if (body.errors && body.errors.length > 0) { 44 | return reject(body.errors); 45 | } 46 | return resolve(body); 47 | }); 48 | }); 49 | }; 50 | }); 51 | } 52 | /* 53 | * There's a V8 bug where, when using Babel, exporting classes with only 54 | * constructors sometimes fails. Until it's patched, this is a solution to 55 | * "ApiClient is not defined" from issue #14. 56 | * https://github.com/erikras/react-redux-universal-hot-example/issues/14 57 | * 58 | * Relevant Babel bug (but they claim it's V8): https://phabricator.babeljs.io/T2455 59 | * 60 | * Remove it at your own risk. 61 | */ 62 | empty() {} 63 | } 64 | -------------------------------------------------------------------------------- /src/common/utils/authRequired.js: -------------------------------------------------------------------------------- 1 | // refer to https://github.com/reactjs/react-router/blob/master/examples%2Fauth-flow%2Fapp.js 2 | export default (store) => (nextState, replace) => { 3 | const { token } = store.getState().cookies; 4 | if (!token) { 5 | replace({ 6 | pathname: '/user/login', 7 | query: { 8 | next: nextState.location.pathname, 9 | }, 10 | }); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/common/utils/composeEnterHooks.js: -------------------------------------------------------------------------------- 1 | // ref: 2 | // - <https://github.com/ReactTraining/react-router/issues/3103> 3 | // - <https://github.com/baronswindle/react-router-compose-hooks/blob/master/index.js> 4 | 5 | export default { 6 | parallel(...hooks) { 7 | let callbacksRequired = hooks.reduce((totalCallbacks, hook) => { 8 | if (hook.length >= 3) { 9 | totalCallbacks++; 10 | } 11 | return totalCallbacks; 12 | }, 0); 13 | 14 | return function onEnter(nextState, replace, executeTransition) { 15 | let callbacksInvoked = 0; 16 | hooks.forEach((hook) => { 17 | hook.call(this, nextState, replace, () => { 18 | if (++callbacksInvoked === callbacksRequired) { 19 | executeTransition(); 20 | } 21 | }); 22 | }); 23 | if (!callbacksRequired) { 24 | executeTransition(); 25 | } 26 | }; 27 | }, 28 | 29 | series(...hooks) { 30 | return function onEnter(nextState, replace, executeTransition) { 31 | (function executeHooksSynchronously(remainingHooks) { 32 | if (!remainingHooks.length) { 33 | return executeTransition(); 34 | } 35 | let nextHook = remainingHooks[0]; 36 | if (nextHook.length >= 3) { 37 | nextHook.call(this, nextState, replace, () => { 38 | executeHooksSynchronously(remainingHooks.slice(1)); 39 | }); 40 | } else { 41 | nextHook.call(this, nextState, replace); 42 | executeHooksSynchronously(remainingHooks.slice(1)); 43 | } 44 | })(hooks); 45 | }; 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/common/utils/deserializeCookieMap.js: -------------------------------------------------------------------------------- 1 | import mapValues from 'lodash/mapValues'; 2 | 3 | export function deserializeCookie(cookieValue) { 4 | try { 5 | let parsed = JSON.parse(cookieValue); 6 | return parsed; 7 | } catch (err) { 8 | return cookieValue; 9 | } 10 | }; 11 | 12 | export default (cookieMap) => mapValues(cookieMap, deserializeCookie); 13 | -------------------------------------------------------------------------------- /src/common/utils/ensure-polyfill.js: -------------------------------------------------------------------------------- 1 | // below is for preventing webpack warning message 2 | // ref: <https://github.com/webpack/webpack/issues/1781>@yurydelendik 3 | if (typeof module !== 'undefined' && module.require) { 4 | if (typeof require.ensure === 'undefined') { 5 | // ref: <https://github.com/webpack/webpack/issues/183>@snadn 6 | let proto = Object.getPrototypeOf(require); 7 | Object.defineProperties(proto, { 8 | ensure: { 9 | value: function ensure(modules, callback) { 10 | callback(this); 11 | }, 12 | writable: false, 13 | }, 14 | include: { 15 | value: function include() {}, 16 | writable: false, 17 | }, 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/common/utils/roleRequired.js: -------------------------------------------------------------------------------- 1 | export default (store) => (requiredRoles) => (nextState, replace) => { 2 | let { user } = store.getState().cookies; 3 | user = user || {}; 4 | 5 | if (!(( 6 | requiredRoles instanceof Array && 7 | requiredRoles.indexOf(user.role) >= 0 8 | ) || ( 9 | user.role === requiredRoles 10 | ))) { 11 | replace({ 12 | pathname: '/', 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/common/utils/toRefreshURL.js: -------------------------------------------------------------------------------- 1 | export default (URL) => { 2 | let forceUpdate = (URL.indexOf('?') >= 0 ? 3 | '&' : '?') + `forceUpdate=${Math.random()}`; 4 | return URL + forceUpdate; 5 | }; 6 | -------------------------------------------------------------------------------- /src/native/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore, applyMiddleware } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import thunk from 'redux-thunk'; 5 | import rootReducer from './reducers'; 6 | import Router from './Router'; 7 | import scenes from './scenes'; 8 | 9 | let store = createStore(rootReducer, applyMiddleware(thunk)); 10 | 11 | let App = () => ( 12 | <Provider store={store}> 13 | <Router> 14 | {scenes} 15 | </Router> 16 | </Provider> 17 | ); 18 | 19 | export default App; 20 | -------------------------------------------------------------------------------- /src/native/Router.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { connect } from 'react-redux'; 3 | import { Router as RNRFRouter, Reducer } from 'react-native-router-flux'; 4 | 5 | class Router extends Component { 6 | // ref: <https://github.com/lynndylanhurley/react-native-router-flux#reduxflux> 7 | reducerCreate(params) { 8 | const defaultReducer = Reducer(params); 9 | 10 | return (state, action) => { 11 | this.props.dispatch(action); 12 | return defaultReducer(state, action); 13 | }; 14 | } 15 | 16 | // ref: <https://github.com/aksonov/react-native-router-flux/blob/master/Example/Example.js> 17 | getSceneStyle(props, computedProps) { 18 | let style = { 19 | flex: 1, 20 | backgroundColor: '#fff', 21 | shadowColor: null, 22 | shadowOffset: null, 23 | shadowOpacity: null, 24 | shadowRadius: null, 25 | }; 26 | if (computedProps.isActive) { 27 | style.marginTop = computedProps.hideNavBar ? 0 : 64; 28 | style.marginBottom = computedProps.hideTabBar ? 0 : 50; 29 | } 30 | return style; 31 | } 32 | 33 | render() { 34 | return ( 35 | <RNRFRouter 36 | createReducer={this.reducerCreate.bind(this)} 37 | getSceneStyle={this.getSceneStyle.bind(this)} 38 | > 39 | {this.props.children} 40 | </RNRFRouter> 41 | ); 42 | } 43 | } 44 | 45 | Router.propTypes = { 46 | dispatch: PropTypes.func, 47 | }; 48 | 49 | export default connect()(Router); 50 | -------------------------------------------------------------------------------- /src/native/components/About.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { Actions } from 'react-native-router-flux'; 4 | import styles from '../styles'; 5 | 6 | let About = () => { 7 | const goToAbout = () => Actions.home(); 8 | 9 | return ( 10 | <View> 11 | <Text style={styles.bigblue}> 12 | About 13 | </Text> 14 | <Text style={styles.red} onPress={goToAbout}> 15 | Click me to go back to home page. 16 | </Text> 17 | </View> 18 | ); 19 | }; 20 | 21 | export default About; 22 | -------------------------------------------------------------------------------- /src/native/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { Actions } from 'react-native-router-flux'; 4 | import { connect } from 'react-redux'; 5 | import styles from '../styles'; 6 | 7 | let Home = ({ routes }) => { 8 | const goToAbout = () => Actions.about({text: 'Fuck you'}); 9 | const goToTab1 = () => Actions.tab1(); 10 | 11 | return ( 12 | <View> 13 | <Text style={styles.bigblue}> 14 | Home 15 | </Text> 16 | <Text> 17 | The current scene is titled {routes.scene.title}. 18 | </Text> 19 | <Text style={styles.red} onPress={goToAbout}> 20 | Go To About 21 | </Text> 22 | <Text style={styles.red} onPress={goToTab1}> 23 | Go To Tab1 24 | </Text> 25 | </View> 26 | ); 27 | }; 28 | 29 | Home.propTypes = { 30 | routes: PropTypes.object, 31 | }; 32 | 33 | export default connect(state => state)(Home); 34 | -------------------------------------------------------------------------------- /src/native/components/TabIcon.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Text } from 'react-native'; 3 | 4 | let TabIcon = ({ selected, title }) => ( 5 | <Text 6 | style={{ color: selected ? 'red' : 'black' }} 7 | > 8 | {title} 9 | </Text> 10 | ); 11 | 12 | TabIcon.propTypes = { 13 | selected: PropTypes.bool, 14 | title: PropTypes.string, 15 | }; 16 | 17 | export default TabIcon; 18 | -------------------------------------------------------------------------------- /src/native/components/TabView.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Text, View } from 'react-native'; 3 | import { Actions } from 'react-native-router-flux'; 4 | 5 | let TabView = ({ title }) => { 6 | let goToHome = () => Actions.home(); 7 | 8 | return ( 9 | <View> 10 | <Text> 11 | TabView 12 | </Text> 13 | <Text> 14 | The current scene is titled {title}. 15 | </Text> 16 | <Text onPress={goToHome}> 17 | Go To Home 18 | </Text> 19 | </View> 20 | ); 21 | }; 22 | 23 | TabView.propTypes = { 24 | title: PropTypes.string, 25 | }; 26 | 27 | export default TabView; 28 | -------------------------------------------------------------------------------- /src/native/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './App'; 3 | 4 | AppRegistry.registerComponent('ExpressReactHmrBoilerplate', () => App); 5 | -------------------------------------------------------------------------------- /src/native/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import routes from './routes'; 3 | 4 | export default combineReducers({ 5 | routes, 6 | }); 7 | -------------------------------------------------------------------------------- /src/native/reducers/routes.js: -------------------------------------------------------------------------------- 1 | import { ActionConst } from 'react-native-router-flux'; 2 | 3 | let initialState = { 4 | scene: {}, 5 | }; 6 | 7 | export default function reducer(state = initialState, action = {}) { 8 | switch (action.type) { 9 | // focus action is dispatched when a new screen comes into focus 10 | case ActionConst.FOCUS: 11 | return { 12 | ...state, 13 | scene: action.scene, 14 | }; 15 | 16 | default: 17 | return state; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/native/scenes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Scene } from 'react-native-router-flux'; 3 | import Home from './components/Home'; 4 | import About from './components/About'; 5 | import TabView from './components/TabView'; 6 | import TabIcon from './components/TabIcon'; 7 | import styles from './styles'; 8 | 9 | export default ( 10 | <Scene key="root"> 11 | <Scene key="home" component={Home} title="Home" /> 12 | <Scene key="about" component={About} title="About" /> 13 | <Scene 14 | key="main" 15 | tabs 16 | initial 17 | tabBarStyle={styles.tabBar} 18 | tabBarSelectedItemStyle={styles.tabBarSelectedItem} 19 | > 20 | <Scene key="tab1" component={TabView} title="Tab #1" icon={TabIcon} /> 21 | <Scene 22 | key="tab2" 23 | component={TabView} 24 | initial 25 | title="Tab #2" 26 | icon={TabIcon} 27 | /> 28 | <Scene 29 | key="tab3" 30 | component={TabView} 31 | hideNavBar 32 | title="Tab #3" 33 | icon={TabIcon} 34 | /> 35 | <Scene key="tab4" component={TabView} title="Tab #4" icon={TabIcon} /> 36 | <Scene key="tab5" component={TabView} title="Tab #5" icon={TabIcon} /> 37 | </Scene> 38 | </Scene> 39 | ); 40 | -------------------------------------------------------------------------------- /src/native/styles/index.scss: -------------------------------------------------------------------------------- 1 | .bigblue { 2 | color: blue; 3 | font-weight: bold; 4 | font-size: 30; 5 | } 6 | 7 | .red { 8 | color: red, 9 | } 10 | 11 | .tabBar { 12 | background-color: #eee; 13 | } 14 | 15 | .tabBarSelectedItem { 16 | background-color: #ddd, 17 | } 18 | -------------------------------------------------------------------------------- /src/public/css/main.css: -------------------------------------------------------------------------------- 1 | .navbar-brand { 2 | color: blue !important; 3 | } 4 | -------------------------------------------------------------------------------- /src/public/img/default-avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/src/public/img/default-avatar.png -------------------------------------------------------------------------------- /src/public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gocreating/express-react-hmr-boilerplate/58a7271ff81b298dd7122abf9c749f3a03233a0b/src/public/img/logo.png -------------------------------------------------------------------------------- /src/server/api/nodemailer.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import nodemailer from 'nodemailer'; 3 | import configs from '../../../configs/project/server'; 4 | 5 | let defaultTransport; 6 | if (configs.nodemailer) { 7 | defaultTransport = configs.nodemailer[process.env.NODE_ENV]; 8 | } 9 | 10 | export default (transport = defaultTransport) => { 11 | let transporter = nodemailer.createTransport(transport); 12 | return { 13 | sendMail: (mailOptions) => new Promise((resolve, reject) => { 14 | mailOptions = assign( 15 | {}, 16 | configs.mailOptions.default, 17 | configs.mailOptions[process.env.NODE_ENV], 18 | mailOptions 19 | ); 20 | transporter.sendMail(mailOptions, (err, info) => { 21 | if (process.env.NODE_ENV !== 'test' && err) { 22 | return reject(err); 23 | } 24 | return resolve(info); 25 | }); 26 | }), 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/server/app.js: -------------------------------------------------------------------------------- 1 | import env from './utils/env'; 2 | // just import the injector and don't use it as normal promise 3 | // otherwise you will go into a deadlock 4 | // (webpackIsomorphicTools is waiting for server's webpack-dev-middleware to compiler, 5 | // and the server is waiting for webpackIsomorphicTools' after-compilation-callback) 6 | import './webpackIsomorphicToolsInjector'; 7 | import express from 'express'; 8 | import mongoose from 'mongoose'; 9 | import configs from '../../configs/project/server'; 10 | import clientConfigs from '../../configs/project/client'; 11 | import middlewares from './middlewares'; 12 | import routes from './routes'; 13 | 14 | const appPromise = new Promise((resolve, reject) => { 15 | const app = express(); 16 | app.set('env', env); 17 | 18 | // error handler for the whole app process 19 | process.on('uncaughtException', (err) => { 20 | console.log('uncaughtException', err); 21 | process.exit(1); 22 | }); 23 | 24 | process.on('unhandledRejection', (reason, p) => { 25 | throw reason; 26 | }); 27 | 28 | // initialize firebase 29 | if (configs.firebase && clientConfigs.firebase) { 30 | let firebase = require('firebase'); 31 | firebase.initializeApp({ 32 | serviceAccount: configs.firebase, 33 | databaseURL: clientConfigs.firebase.databaseURL, 34 | }); 35 | if (env !== 'test') { 36 | console.log('[Service] [Firebase]\tenabled'); 37 | } 38 | } else { 39 | if (env !== 'test') { 40 | console.log('[Service] [Firebase]\tdisabled'); 41 | } 42 | } 43 | 44 | // connect to mongolab 45 | if (configs.mongo) { 46 | mongoose.connect(configs.mongo[env], (err) => { 47 | if (err) { 48 | throw err; 49 | } 50 | if (env !== 'test') { 51 | console.log('[Service] [Mongo]\tenabled'); 52 | } 53 | middlewares({ app }); 54 | routes({ app }); 55 | // error handler for the current request 56 | app.use((err, req, res, next) => { 57 | console.error(err.stack); 58 | if (env !== 'production') { 59 | res.status(500).send(`<pre>${err.stack}</pre>`); 60 | } else { 61 | res.status(500).send('Service Unavailable'); 62 | } 63 | }); 64 | return resolve(app); 65 | }); 66 | } else { 67 | if (env !== 'test') { 68 | console.log('[Service] [Mongo]\tdisabled'); 69 | } 70 | return reject(new Error('MongoDB URI is required')); 71 | } 72 | }); 73 | 74 | export default appPromise; 75 | -------------------------------------------------------------------------------- /src/server/components/Html.js: -------------------------------------------------------------------------------- 1 | import serialize from 'serialize-javascript'; 2 | import React, { PropTypes } from 'react'; 3 | import { renderToString } from 'react-dom/server'; 4 | 5 | // jscs:disable 6 | const Html = ({ assets, children, initialState }) => ( 7 | <html lang="utf-8"> 8 | <head> 9 | <meta charSet="utf-8"/> 10 | <title>Express-React-HMR-Boilerplate</title> 11 | <link 12 | rel="stylesheet" 13 | type="text/css" 14 | href="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.css" 15 | /> 16 | <link 17 | rel="stylesheet" 18 | type="text/css" 19 | href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" 20 | /> 21 | <script 22 | src="https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js" 23 | /> 24 | {Object.keys(assets.styles).map((style, i) => 25 | <link 26 | key={i} 27 | href={assets.styles[style]} 28 | media="screen, projection" 29 | rel="stylesheet" 30 | type="text/css" 31 | />)} 32 | </head> 33 | 34 | <body> 35 | <div id="root" 36 | dangerouslySetInnerHTML={{ 37 | __html: renderToString(children), 38 | }} 39 | /> 40 | 41 | <script 42 | dangerouslySetInnerHTML={{ 43 | __html: `window.__INITIAL_STATE__=${ 44 | serialize(initialState, { isJSON: true }) 45 | };`, 46 | }} 47 | /> 48 | <script src="https://code.jquery.com/jquery-2.2.3.min.js" /> 49 | <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en" /> 50 | {Object.keys(assets.javascript).map((script, i) => 51 | <script key={i} src={assets.javascript[script]} /> 52 | )} 53 | </body> 54 | </html> 55 | ); 56 | // jscs:enable 57 | 58 | Html.propTypes = { 59 | assets: PropTypes.object, 60 | component: PropTypes.object, 61 | initialState: PropTypes.object, 62 | }; 63 | 64 | export default Html; 65 | -------------------------------------------------------------------------------- /src/server/components/ResetPasswordMail.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import tokenToURL from '../utils/tokenToURL'; 3 | 4 | let ResetPasswordMail = ({ requestedAt, token }) => { 5 | let url = tokenToURL('/user/password/reset', token); 6 | 7 | return ( 8 | <div> 9 | <p> 10 | Someone requested to reset your password at 11 | {' '}<time>{requestedAt.toString()}</time>. 12 | If you didn't ask for such request, please ignore this mail. 13 | </p> 14 | <p> 15 | Please follow the link to reset your email: 16 | </p> 17 | <p> 18 | <a href={url}> 19 | {url} 20 | </a> 21 | </p> 22 | </div> 23 | ); 24 | }; 25 | 26 | ResetPasswordMail.propTypes = { 27 | requestedAt: PropTypes.instanceOf(Date), 28 | token: PropTypes.string, 29 | }; 30 | 31 | export default ResetPasswordMail; 32 | -------------------------------------------------------------------------------- /src/server/components/VerifyEmailMail.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import tokenToURL from '../utils/tokenToURL'; 3 | 4 | let VerifyEmailMail = ({ token }) => { 5 | let url = tokenToURL('/user/email/verify', token); 6 | 7 | return ( 8 | <div> 9 | <p> 10 | Please click the following link to verify your account. 11 | </p> 12 | <p> 13 | <a href={url}> 14 | {url} 15 | </a> 16 | </p> 17 | </div> 18 | ); 19 | }; 20 | 21 | VerifyEmailMail.propTypes = { 22 | token: PropTypes.string, 23 | }; 24 | 25 | export default VerifyEmailMail; 26 | -------------------------------------------------------------------------------- /src/server/constants/ErrorTypes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | ODM_OPERATION: 'ODM_OPERATION', 3 | JSON_WEB_TOKEN: 'JSON_WEB_TOKEN', 4 | PASSPORT: 'PASSPORT', 5 | }; 6 | -------------------------------------------------------------------------------- /src/server/controllers/firebase.js: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase'; 2 | 3 | export default { 4 | readToken(req, res) { 5 | // ref: <https://firebase.google.com/docs/auth/server#create_a_custom_token> 6 | let token = firebase.auth().createCustomToken(req.user._id.toString()); 7 | res.json({ 8 | token: token, 9 | }); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/server/controllers/formValidation.js: -------------------------------------------------------------------------------- 1 | import FormNames from '../../common/constants/FormNames'; 2 | import { handleDbError } from '../decorators/handleError'; 3 | import User from '../models/User'; 4 | 5 | export default { 6 | [FormNames.USER_REGISTER]: { 7 | email(req, res) { 8 | User.findOne({ 9 | 'email.value': req.body.value, 10 | }, handleDbError(res)((user) => { 11 | if (user) { 12 | res.json({ 13 | isPassed: false, 14 | message: 'The email is already registered', 15 | }); 16 | } else { 17 | res.json({ 18 | isPassed: true, 19 | }); 20 | } 21 | })); 22 | }, 23 | }, 24 | 25 | [FormNames.USER_VERIFY_EMAIL]: { 26 | email(req, res) { 27 | User.findOne({ 28 | 'email.value': req.body.value, 29 | }, handleDbError(res)((user) => { 30 | if (!user) { 31 | res.json({ 32 | isPassed: false, 33 | message: 'This is an invalid account', 34 | }); 35 | } else { 36 | res.json({ 37 | isPassed: true, 38 | }); 39 | } 40 | })); 41 | }, 42 | }, 43 | 44 | [FormNames.USER_FORGET_PASSWORD]: { 45 | email(req, res) { 46 | User.findOne({ 47 | 'email.value': req.body.value, 48 | }, handleDbError(res)((user) => { 49 | if (!user) { 50 | res.json({ 51 | isPassed: false, 52 | message: 'This is an invalid account', 53 | }); 54 | } else { 55 | res.json({ 56 | isPassed: true, 57 | }); 58 | } 59 | })); 60 | }, 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /src/server/controllers/locale.js: -------------------------------------------------------------------------------- 1 | import Errors from '../../common/constants/Errors'; 2 | 3 | export default { 4 | show(req, res) { 5 | try { 6 | // escape file path for security 7 | const locale = req.params.locale 8 | .replace(/\./g, '') 9 | .replace(/\//g, '') 10 | .toLowerCase(); 11 | const messages = require(`../../common/i18n/${locale}`).default; 12 | 13 | res.json({ 14 | locale, 15 | messages, 16 | }); 17 | } catch (e) { 18 | res.errors([Errors.LOCALE_NOT_SUPPORTED]); 19 | } 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/server/controllers/mail.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToString } from 'react-dom/server'; 3 | import Errors from '../../common/constants/Errors'; 4 | import nodemailerAPI from '../api/nodemailer'; 5 | import VerifyEmailMail from '../components/VerifyEmailMail'; 6 | import ResetPasswordMail from '../components/ResetPasswordMail'; 7 | 8 | export default { 9 | sendVerification(req, res) { 10 | let { user } = req; 11 | let token = user.toVerifyEmailToken(); 12 | 13 | nodemailerAPI() 14 | .sendMail({ 15 | ...( 16 | process.env.NODE_ENV === 'production' ? 17 | { to: user.email.value } : 18 | {} 19 | ), 20 | subject: 'Email Verification', 21 | html: renderToString( 22 | <VerifyEmailMail token={token} /> 23 | ), 24 | }) 25 | .catch((err) => { 26 | res.errors([Errors.SEND_EMAIL_FAIL]); 27 | throw err; 28 | }) 29 | .then((info) => { 30 | res.json({ 31 | user: user, 32 | email: info && info.envelope, 33 | }); 34 | }); 35 | }, 36 | 37 | sendResetPasswordLink(req, res) { 38 | let { user } = req; 39 | let token = user.toResetPasswordToken(); 40 | 41 | nodemailerAPI() 42 | .sendMail({ 43 | ...( 44 | process.env.NODE_ENV === 'production' ? 45 | { to: user.email.value } : 46 | {} 47 | ), 48 | subject: 'Reset Password Request', 49 | html: renderToString( 50 | <ResetPasswordMail 51 | requestedAt={new Date()} 52 | token={token} 53 | /> 54 | ), 55 | }) 56 | .catch((err) => { 57 | res.errors([Errors.SEND_EMAIL_FAIL]); 58 | throw err; 59 | }) 60 | .then((info) => { 61 | res.json({ 62 | email: info.envelope, 63 | }); 64 | }); 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /src/server/controllers/react.js: -------------------------------------------------------------------------------- 1 | import env from '../utils/env'; 2 | import React from 'react'; 3 | import { renderToString } from 'react-dom/server'; 4 | import { match, RouterContext } from 'react-router'; 5 | import { Provider } from 'react-redux'; 6 | import LocaleProvider from '../../common/components/utils/LocaleProvider'; 7 | import Html from '../components/Html'; 8 | import getRoutes from '../../common/routes'; 9 | 10 | export default { 11 | render(req, res) { 12 | if (env === 'development') { 13 | __webpackIsomorphicTools__.refresh(); 14 | } 15 | let routes = getRoutes(req.store); 16 | match({ 17 | routes, 18 | // we use `history: req.history` instead of `location: req.url` to deal with redirections 19 | history: req.history, 20 | }, (error, redirectLocation, renderProps) => { 21 | if (error) { 22 | return res.status(500).send(error.message); 23 | } 24 | if (redirectLocation) { 25 | return res.redirect( 26 | 302, redirectLocation.pathname + redirectLocation.search); 27 | } 28 | if (renderProps == null) { 29 | return res.status(404).send('Not found'); 30 | } 31 | const finalState = req.store.getState(); 32 | const markup = '<!doctype html>\n' + renderToString( 33 | <Html 34 | initialState={finalState} 35 | assets={__webpackIsomorphicTools__.assets()} 36 | > 37 | <Provider store={req.store}> 38 | <LocaleProvider> 39 | <RouterContext {...renderProps} /> 40 | </LocaleProvider> 41 | </Provider> 42 | </Html> 43 | ); 44 | res.send(markup); 45 | }); 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /src/server/controllers/socialAuth.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | 3 | export default { 4 | setupError: (req, res) => { 5 | res.send( 6 | 'Please setup and turn on `passportStrategy.&lt;social provider&gt;` ' + 7 | 'of config file `configs/project/server.js`' 8 | ); 9 | }, 10 | initFacebook: (req, res, next) => ( 11 | passport.authenticate('facebook', { 12 | scope: ['public_profile', 'email'], 13 | state: JSON.stringify({ next: req.query.next }), 14 | })(req, res, next) 15 | ), 16 | initLinkedin: (req, res, next) => ( 17 | passport.authenticate('linkedin', { 18 | state: JSON.stringify({ 19 | next: req.query.next, 20 | random: Math.random(), 21 | }), 22 | })(req, res, next) 23 | ), 24 | }; 25 | -------------------------------------------------------------------------------- /src/server/controllers/ssrFetchState.js: -------------------------------------------------------------------------------- 1 | import Errors from '../../common/constants/Errors'; 2 | import todoAPI from '../../common/api/todo'; 3 | import wrapTimeout from '../decorators/wrapTimeout'; 4 | import { loginUser } from '../../common/actions/userActions'; 5 | import { updateLocale } from '../../common/actions/intlActions'; 6 | import { setTodos } from '../../common/actions/todoActions'; 7 | 8 | export default { 9 | user: (req, res, next) => { 10 | let { cookies } = req.store.getState(); 11 | req.store.dispatch(loginUser({ 12 | token: cookies.token, 13 | data: cookies.user, 14 | })); 15 | next(); 16 | }, 17 | intl: wrapTimeout(3000)((req, res, next) => { 18 | const cookieLocale = req.store.getState().cookies.locale; 19 | let lang; 20 | if (cookieLocale) { 21 | lang = cookieLocale; 22 | } else { 23 | lang = req.acceptsLanguages('en-us', 'zh-tw'); 24 | } 25 | req.store 26 | .dispatch(updateLocale(lang)) 27 | .then(() => { 28 | next(); 29 | }, () => { 30 | res.pushError(Errors.STATE_PRE_FETCHING_FAIL, { 31 | detail: 'Cannot setup locale', 32 | }); 33 | next(); 34 | }); 35 | }), 36 | todo: wrapTimeout(3000)((req, res, next) => { 37 | todoAPI(req.store.getState().apiEngine) 38 | .list({ page: req.query.page || 1 }) 39 | .catch(() => { 40 | res.pushError(Errors.STATE_PRE_FETCHING_FAIL, { 41 | detail: 'Cannot list todos', 42 | }); 43 | next(); 44 | }) 45 | .then((json) => { 46 | req.store.dispatch(setTodos(json)); 47 | next(); 48 | }); 49 | }), 50 | }; 51 | -------------------------------------------------------------------------------- /src/server/controllers/todo.js: -------------------------------------------------------------------------------- 1 | import assign from 'object-assign'; 2 | import { handleDbError } from '../decorators/handleError'; 3 | import filterAttribute from '../utils/filterAttribute'; 4 | import Todo from '../models/Todo'; 5 | 6 | export default { 7 | list(req, res) { 8 | Todo.paginate({ 9 | page: req.query.page, 10 | perPage: 5, 11 | }, handleDbError(res)((page) => { 12 | Todo 13 | .find({}, null, { 14 | limit: page.limit, 15 | skip: page.skip < 0 ? 0 : page.skip, 16 | sort: { createdAt: 'desc' }, 17 | }) 18 | .then((todos) => { 19 | res.json({ 20 | todos: todos, 21 | page: page, 22 | }); 23 | }); 24 | })); 25 | }, 26 | 27 | create(req, res) { 28 | const todo = Todo({ 29 | text: req.body.text, 30 | }); 31 | 32 | todo.save(handleDbError(res)((todo) => { 33 | res.json({ 34 | todo: todo, 35 | }); 36 | })); 37 | }, 38 | 39 | update(req, res) { 40 | let modifiedTodo = filterAttribute(req.body, [ 41 | 'text', 42 | ]); 43 | 44 | Todo.findById(req.params.id, handleDbError(res)((todo) => { 45 | todo = assign(todo, modifiedTodo); 46 | todo.save(handleDbError(res)(() => { 47 | res.json({ 48 | originAttributes: req.body, 49 | updatedAttributes: todo, 50 | }); 51 | })); 52 | })); 53 | }, 54 | 55 | remove(req, res) { 56 | Todo.remove({_id: req.params.id}, handleDbError(res)(() => { 57 | res.json({}); 58 | })); 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /src/server/decorators/handleError.js: -------------------------------------------------------------------------------- 1 | import ErrorTypes from '../constants/ErrorTypes'; 2 | import Errors from '../../common/constants/Errors'; 3 | 4 | let getErrorHandler = (errorTypes) => (res) => (fn) => (err, ...result) => { 5 | if (err) { 6 | if (!Array.isArray(errorTypes)) { 7 | errorTypes = [errorTypes]; 8 | } 9 | errorTypes.forEach((errorType) => { 10 | switch (errorType) { 11 | case ErrorTypes.ODM_OPERATION: { 12 | if (err.name === 'ValidationError') { 13 | res.pushError(Errors.ODM_VALIDATION, err); 14 | return res.errors(); 15 | } 16 | if (err.name === 'MongooseError') { 17 | res.pushError(Errors.ODM_OPERATION_FAIL, err); 18 | return res.errors(); 19 | } 20 | break; 21 | } 22 | case ErrorTypes.JSON_WEB_TOKEN: { 23 | // ref: 24 | // - <https://github.com/auth0/node-jsonwebtoken#errors--codes> 25 | if (err.name === 'JsonWebTokenError') { 26 | res.pushError(Errors.BAD_TOKEN, err); 27 | return res.errors(); 28 | } else if (err.name === 'TokenExpiredError') { 29 | res.pushError(Errors.TOKEN_EXPIRATION, err); 30 | return res.errors(); 31 | } 32 | break; 33 | } 34 | case ErrorTypes.PASSPORT: { 35 | if (err.message === 'No auth token') { 36 | res.pushError(Errors.USER_UNAUTHORIZED, err); 37 | return res.errors(); 38 | } 39 | break; 40 | } 41 | default: { 42 | res.pushError(Errors.UNKNOWN_EXCEPTION, err); 43 | return res.errors(); 44 | } 45 | } 46 | }); 47 | } else { 48 | fn(...result); 49 | } 50 | }; 51 | 52 | let handleError = getErrorHandler(null); 53 | let handleDbError = getErrorHandler(ErrorTypes.ODM_OPERATION); 54 | let handleJwtError = getErrorHandler(ErrorTypes.JSON_WEB_TOKEN); 55 | let handlePassportError = getErrorHandler([ 56 | ErrorTypes.JSON_WEB_TOKEN, 57 | ErrorTypes.PASSPORT, 58 | ]); 59 | 60 | export { 61 | handleDbError, 62 | handleJwtError, 63 | handlePassportError, 64 | }; 65 | export default handleError; 66 | -------------------------------------------------------------------------------- /src/server/decorators/wrapTimeout.js: -------------------------------------------------------------------------------- 1 | const wrapTimeout = (milliseconds) => (fn) => (req, res, next) => { 2 | let t = setTimeout(() => { 3 | console.log('-- time out --'); 4 | console.log('url:', req.url); 5 | console.log('--------------'); 6 | next(); 7 | }, milliseconds); 8 | let done = (...args) => { 9 | clearTimeout(t); 10 | next(...args); 11 | }; 12 | fn(req, res, done); 13 | }; 14 | 15 | export default wrapTimeout; 16 | -------------------------------------------------------------------------------- /src/server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-register')(require('../../configs/env/babel.config.dev.server')); 2 | require('./server'); 3 | -------------------------------------------------------------------------------- /src/server/middlewares/authRequired.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import handleError, { handlePassportError } from '../decorators/handleError'; 3 | import Errors from '../../common/constants/Errors'; 4 | 5 | const authRequired = (req, res, next) => { 6 | passport.authenticate( 7 | 'jwt', 8 | { session: false }, 9 | handleError(res)((user, info) => { 10 | handlePassportError(res)((user) => { 11 | if (!user) { 12 | res.pushError(Errors.USER_UNAUTHORIZED); 13 | return res.errors(); 14 | } 15 | req.user = user; 16 | next(); 17 | })(info, user); 18 | }) 19 | )(req, res, next); 20 | }; 21 | 22 | export default authRequired; 23 | -------------------------------------------------------------------------------- /src/server/middlewares/bodyParser.js: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import jwt from 'jsonwebtoken'; 3 | import { handleJwtError } from '../decorators/handleError'; 4 | 5 | export default { 6 | // parse application/x-www-form-urlencoded 7 | urlencoded: bodyParser.urlencoded({ extended: false }), 8 | // parse application/json 9 | json: bodyParser.json(), 10 | jwt: (key, secret) => (req, res, next) => { 11 | let token = req.body[key]; 12 | 13 | jwt.verify(token, secret, handleJwtError(res)((decoded) => { 14 | req.decodedPayload = decoded; 15 | next(); 16 | })); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/server/middlewares/fileUpload.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import multer from 'multer'; 3 | import mkdirp from 'mkdirp'; 4 | 5 | // parse multipart/form-data 6 | 7 | const initDestination = 'uploads'; 8 | let uploadToDisk = ({ 9 | destination = initDestination, 10 | filename, 11 | }) => multer({ 12 | storage: multer.diskStorage({ 13 | destination: (req, file, cb) => { 14 | if (req.user) { 15 | destination = destination.replace('{userId}', req.user._id); 16 | } 17 | let dir = path.join(__dirname, `../../public/${destination}`); 18 | mkdirp(dir, (err) => cb(err, dir)); 19 | }, 20 | filename: (req, file, cb) => { 21 | cb(null, filename || file.fieldname + '-' + Date.now()); 22 | }, 23 | }), 24 | }); 25 | 26 | let uploadToMemory = multer({ 27 | storage: multer.memoryStorage(), 28 | }); 29 | 30 | export default { 31 | disk: uploadToDisk, 32 | memory: uploadToMemory, 33 | }; 34 | -------------------------------------------------------------------------------- /src/server/middlewares/index.js: -------------------------------------------------------------------------------- 1 | import env from '../utils/env'; 2 | import path from 'path'; 3 | import express from 'express'; 4 | import favicon from 'serve-favicon'; 5 | import morgan from './morgan'; 6 | import passportInit from './passportInit'; 7 | import mountStore from './mountStore'; 8 | import mountHelper from './mountHelper'; 9 | import initCookie from './initCookie'; 10 | 11 | export default ({ app }) => { 12 | // inject livereload feature 13 | if (env === 'development') { 14 | console.log('using livereload'); 15 | const webpack = require('webpack'); 16 | const config = require('../../../configs/env/webpack.config.dev'); 17 | const compiler = webpack(config); 18 | 19 | app.use(require('webpack-dev-middleware')(compiler, { 20 | noInfo: true, 21 | publicPath: config.output.publicPath, 22 | })); 23 | 24 | app.use(require('webpack-hot-middleware')(compiler)); 25 | } 26 | 27 | // favicon 28 | app.use(favicon(path.join(__dirname, '../../public/img/logo.png'))); 29 | 30 | // log request 31 | app.use(morgan); 32 | 33 | // static files 34 | app.use(express.static( 35 | path.join(__dirname, '../../public'))); 36 | 37 | // mount redux store 38 | app.use(mountStore); 39 | 40 | // mount custom helpers 41 | app.use(mountHelper); 42 | 43 | // initialize cookie 44 | app.use(initCookie); 45 | 46 | // setup passport 47 | app.use(passportInit); 48 | }; 49 | -------------------------------------------------------------------------------- /src/server/middlewares/initCookie.js: -------------------------------------------------------------------------------- 1 | import cookie from 'cookie'; 2 | import { setCookies } from '../../common/actions/cookieActions'; 3 | 4 | export default (req, res, next) => { 5 | if (req.headers.cookie !== undefined) { 6 | let c = cookie.parse(req.headers.cookie); 7 | req.store.dispatch(setCookies(c)); 8 | } 9 | next(); 10 | }; 11 | -------------------------------------------------------------------------------- /src/server/middlewares/morgan.js: -------------------------------------------------------------------------------- 1 | import env from '../utils/env'; 2 | import morgan from 'morgan'; 3 | import passMiddleware from './pass'; 4 | 5 | morgan.token('colorStatus', (req, res) => { 6 | const status = res.statusCode; 7 | let color = ''; 8 | 9 | if (status < 200) { 10 | // 1xx 11 | color = '\x1b[0m'; 12 | } else if (status < 300) { 13 | // 2xx 14 | color = '\x1b[0;32m'; 15 | } else if (status < 400) { 16 | // 3xx 17 | color = '\x1b[1;33m'; 18 | } else if (status < 500) { 19 | // 4xx 20 | color = '\x1b[0;31m'; 21 | } else { 22 | // 5xx 23 | color = '\x1b[0;35m'; 24 | } 25 | 26 | return color + status + '\x1b[0m'; 27 | }); 28 | 29 | let morganMiddleware = null; 30 | if (env === 'development') { 31 | morganMiddleware = morgan( 32 | '\x1b[1;30m' + '[:date[iso]] ' + 33 | '\x1b[0m' + ':remote-addr\t' + 34 | ':colorStatus ' + 35 | ':method ' + 36 | ':url\t' + 37 | '\x1b[0m' + ':res[content-length] - ' + 38 | '\x1b[0;36m' + ':response-time ms' + 39 | '\x1b[0m' 40 | ); 41 | } else if (env === 'test') { 42 | morganMiddleware = passMiddleware; 43 | } else if (env === 'production') { 44 | morganMiddleware = morgan( 45 | '[:date[iso]] ' + 46 | ':remote-addr\t' + 47 | ':status ' + 48 | ':method ' + 49 | ':url\t' + 50 | ':res[content-length] - ' + 51 | ':response-time ms' 52 | ); 53 | } 54 | export default morganMiddleware; 55 | -------------------------------------------------------------------------------- /src/server/middlewares/mountHelper.js: -------------------------------------------------------------------------------- 1 | import { pushErrors } from '../../common/actions/errorActions'; 2 | 3 | export default (req, res, next) => { 4 | res.pushError = (error, meta) => { 5 | req.store.dispatch(pushErrors([{ 6 | ...error, 7 | meta: { 8 | path: req.path, 9 | ...meta, 10 | }, 11 | }])); 12 | }; 13 | res.errors = (errors) => { 14 | req.store.dispatch(pushErrors(errors)); 15 | res.json({ 16 | errors: req.store.getState().errors.map((error) => { 17 | delete error.id; 18 | return { 19 | ...error, 20 | meta: { 21 | path: req.path, 22 | ...error.meta, 23 | }, 24 | }; 25 | }), 26 | }); 27 | }; 28 | 29 | next(); 30 | }; 31 | -------------------------------------------------------------------------------- /src/server/middlewares/mountStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { useRouterHistory, createMemoryHistory } from 'react-router'; 4 | import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux'; 5 | import rootReducer from '../../common/reducers'; 6 | import ApiEngine from '../../common/utils/ApiEngine'; 7 | import { setApiEngine } from '../../common/actions/apiEngine'; 8 | 9 | export default (req, res, next) => { 10 | // ref: 11 | // - <https://github.com/reactjs/react-router-redux/issues/182#issuecomment-178701502> 12 | // - <http://stackoverflow.com/questions/34821921/browserhistory-undefined-with-react-router-2-00-release-candidates> 13 | // - <https://github.com/reactjs/react-router-redux/blob/master/examples/server/server.js> 14 | let memoryHistory = useRouterHistory(createMemoryHistory)(req.url); 15 | let store = createStore( 16 | rootReducer, 17 | applyMiddleware( 18 | routerMiddleware(memoryHistory), 19 | thunk 20 | ) 21 | ); 22 | let history = syncHistoryWithStore(memoryHistory, store); 23 | req.store = store; 24 | req.history = history; 25 | let apiEngine = new ApiEngine(req); 26 | req.store.dispatch(setApiEngine(apiEngine)); 27 | next(); 28 | }; 29 | -------------------------------------------------------------------------------- /src/server/middlewares/pass.js: -------------------------------------------------------------------------------- 1 | export default (req, res, next) => { 2 | next(); 3 | }; 4 | -------------------------------------------------------------------------------- /src/server/middlewares/passportAuth.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import handleError from '../decorators/handleError'; 3 | 4 | export default (strategyName) => (req, res, next) => ( 5 | passport.authenticate(strategyName, { 6 | failureRedirect: '/user/login', 7 | session: false, 8 | }, handleError(res)((user, info) => { 9 | // mount user instance 10 | req.user = user; 11 | next(); 12 | }))(req, res, next) 13 | ); 14 | -------------------------------------------------------------------------------- /src/server/middlewares/passportInit.js: -------------------------------------------------------------------------------- 1 | import passport from 'passport'; 2 | import { Strategy as JwtStrategy } from 'passport-jwt'; 3 | import { Strategy as FacebookStrategy } from 'passport-facebook'; 4 | import { Strategy as OAuthLinkedinStrategy } from 'passport-linkedin-oauth2'; 5 | import configs from '../../../configs/project/server'; 6 | import { redirect } from '../../common/actions/routeActions'; 7 | import Errors from '../../common/constants/Errors'; 8 | import { handleDbError } from '../decorators/handleError'; 9 | import User from '../models/User'; 10 | 11 | let cookieExtractor = (req) => { 12 | return req.store.getState().cookies.token; 13 | }; 14 | 15 | export default (req, res, next) => { 16 | function findOrCreateUser(schemaProfileKey, email, cb) { 17 | if (!email) { 18 | res.pushError(Errors.AUTHORIZATION_FAIL, { 19 | requiredFields: ['email'], 20 | }); 21 | req.store.dispatch(redirect('/user/login')); 22 | return next(); 23 | } 24 | User.findOne({ 'email.value': email }, (err, user) => { 25 | if (err) { 26 | return cb(err); 27 | } 28 | if (!user) { 29 | user = new User({ 30 | avatarURL: '', // overwrite default avatar 31 | }); 32 | } 33 | if (!user.social.profile[schemaProfileKey]) { 34 | user.social.profile[schemaProfileKey] = {}; 35 | } 36 | return cb(null, user); 37 | }); 38 | } 39 | 40 | passport.use(new JwtStrategy({ 41 | jwtFromRequest: cookieExtractor, 42 | secretOrKey: configs.jwt.authentication.secret, 43 | }, (jwtPayload, done) => { 44 | // this callback is invoked only when jwt token is correctly decoded 45 | User.findById(jwtPayload._id, handleDbError(res)((user) => { 46 | done(null, user); 47 | })); 48 | })); 49 | 50 | if (configs.passportStrategy.facebook) { 51 | passport.use(new FacebookStrategy({ 52 | ...configs.passportStrategy.facebook.default, 53 | ...configs.passportStrategy.facebook[process.env.NODE_ENV], 54 | }, (req, accessToken, refreshToken, profile, done) => { 55 | findOrCreateUser( 56 | 'facebook', 57 | profile._json.email, 58 | handleDbError(res)((user) => { 59 | // map `facebook-specific` profile fields to our custom profile fields 60 | user.social.profile.facebook = profile._json; 61 | user.email.value = user.email.value || profile._json.email; 62 | user.name = user.name || profile._json.name; 63 | user.avatarURL = user.avatarURL || profile._json.picture.data.url; 64 | done(null, user); 65 | })); 66 | })); 67 | } 68 | 69 | if (configs.passportStrategy.linkedin) { 70 | passport.use(new OAuthLinkedinStrategy({ 71 | ...configs.passportStrategy.linkedin.default, 72 | ...configs.passportStrategy.linkedin[process.env.NODE_ENV], 73 | }, (req, accessToken, refreshToken, profile, done) => { 74 | findOrCreateUser( 75 | 'linkedin', 76 | profile._json.emailAddress, 77 | handleDbError(res)((user) => { 78 | // map `linkedin-specific` profile fields to our custom profile fields 79 | user.social.profile.linkedin = profile._json; 80 | user.email.value = user.email.value || profile._json.emailAddress; 81 | user.name = user.name || profile._json.formattedName; 82 | user.avatarURL = user.avatarURL || profile._json.pictureUrl; 83 | done(null, user); 84 | }) 85 | ); 86 | })); 87 | } 88 | 89 | passport.initialize()(req, res, next); 90 | }; 91 | -------------------------------------------------------------------------------- /src/server/middlewares/roleRequired.js: -------------------------------------------------------------------------------- 1 | import Errors from '../../common/constants/Errors'; 2 | 3 | const roleRequired = (requiredRoles) => (req, res, next) => { 4 | if (( 5 | requiredRoles instanceof Array && 6 | requiredRoles.indexOf(req.user.role) >= 0 7 | ) || ( 8 | req.user.role === requiredRoles 9 | )) { 10 | next(); 11 | } else { 12 | return res.errors([Errors.PERMISSION_DENIED]); 13 | } 14 | }; 15 | 16 | export default roleRequired; 17 | -------------------------------------------------------------------------------- /src/server/middlewares/validate.js: -------------------------------------------------------------------------------- 1 | import superagent from 'superagent'; 2 | import Errors from '../../common/constants/Errors'; 3 | import serverConfigs from '../../../configs/project/server'; 4 | import clientConfigs from '../../../configs/project/client'; 5 | import validateErrorObject from '../utils/validateErrorObject'; 6 | import { handleDbError } from '../decorators/handleError'; 7 | import User from '../models/User'; 8 | 9 | export default { 10 | form: (formPath, onlyFields = []) => (req, res, next) => { 11 | let { validate } = require(`../../common/components/forms/${formPath}`); 12 | let errors = validate({ 13 | ...req.body, 14 | ...req.files, 15 | }); 16 | 17 | if (onlyFields.length > 0) { 18 | let newErrors = {}; 19 | onlyFields.forEach((field) => { 20 | newErrors[field] = errors[field]; 21 | }); 22 | errors = newErrors; 23 | } 24 | 25 | if (!validateErrorObject(errors)) { 26 | res.pushError(Errors.INVALID_DATA, { 27 | errors, 28 | }); 29 | return res.errors(); 30 | } 31 | next(); 32 | }, 33 | 34 | verifyUserNonce: (nonceKey) => (req, res, next) => { 35 | let { _id, nonce } = req.decodedPayload; 36 | User.findById(_id, handleDbError(res)((user) => { 37 | if (nonce !== user.nonce[nonceKey]) { 38 | return res.errors([Errors.TOKEN_REUSED]); 39 | } 40 | user.nonce[nonceKey] = -1; 41 | req.user = user; 42 | next(); 43 | })); 44 | }, 45 | 46 | recaptcha(req, res, next) { 47 | if (process.env.NODE_ENV === 'test' || !clientConfigs.recaptcha) { 48 | return next(); 49 | } 50 | superagent 51 | .post('https://www.google.com/recaptcha/api/siteverify') 52 | .type('form') 53 | .send({ 54 | secret: serverConfigs.recaptcha[process.env.NODE_ENV].secretKey, 55 | response: req.body.recaptcha, 56 | }) 57 | .end((err, { body } = {}) => { 58 | if (err) { 59 | res.pushError(Errors.UNKNOWN_EXCEPTION, { 60 | meta: err, 61 | }); 62 | return res.errors(); 63 | } 64 | if (!body.success) { 65 | res.pushError(Errors.INVALID_RECAPTCHA, { 66 | meta: body['error-codes'], 67 | }); 68 | return res.errors(); 69 | } 70 | next(); 71 | }); 72 | }, 73 | }; 74 | -------------------------------------------------------------------------------- /src/server/models/Todo.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | import paginatePlugin from './plugins/paginate'; 3 | 4 | let Todo = new mongoose.Schema({ 5 | text: String, 6 | }, { 7 | versionKey: false, 8 | timestamps: { 9 | createdAt: 'createdAt', 10 | updatedAt: 'updatedAt', 11 | }, 12 | }); 13 | 14 | Todo.plugin(paginatePlugin); 15 | 16 | export default mongoose.model('Todo', Todo); 17 | -------------------------------------------------------------------------------- /src/server/models/User.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import mongoose from 'mongoose'; 3 | import jwt from 'jsonwebtoken'; 4 | import configs from '../../../configs/project/server'; 5 | import Roles from '../../common/constants/Roles'; 6 | import paginatePlugin from './plugins/paginate'; 7 | 8 | const hashPassword = (rawPassword = '') => { 9 | let recursiveLevel = 5; 10 | while (recursiveLevel) { 11 | rawPassword = crypto 12 | .createHash('md5') 13 | .update(rawPassword) 14 | .digest('hex'); 15 | recursiveLevel -= 1; 16 | } 17 | return rawPassword; 18 | }; 19 | 20 | let UserSchema = new mongoose.Schema({ 21 | name: String, 22 | email: { 23 | value: { 24 | type: String, 25 | required: true, 26 | }, 27 | isVerified: { 28 | type: Boolean, 29 | default: false, 30 | }, 31 | verifiedAt: Date, 32 | }, 33 | password: { 34 | type: String, 35 | // there is no password for a social account 36 | required: false, 37 | set: hashPassword, 38 | }, 39 | role: { 40 | type: String, 41 | enum: Object.keys(Roles).map(r => Roles[r]), 42 | default: Roles.USER, 43 | }, 44 | avatarURL: { 45 | type: String, 46 | default: '/img/default-avatar.png', 47 | }, 48 | social: { 49 | profile: { 50 | facebook: Object, 51 | linkedin: Object, 52 | }, 53 | }, 54 | nonce: { 55 | verifyEmail: Number, 56 | resetPassword: Number, 57 | }, 58 | lastLoggedInAt: Date, 59 | }, { 60 | versionKey: false, 61 | timestamps: { 62 | createdAt: 'createdAt', 63 | updatedAt: 'updatedAt', 64 | }, 65 | }); 66 | 67 | UserSchema.plugin(paginatePlugin); 68 | 69 | UserSchema.methods.auth = function(password, cb) { 70 | const isAuthenticated = (this.password === hashPassword(password)); 71 | cb(null, isAuthenticated); 72 | }; 73 | 74 | UserSchema.methods.toVerifyEmailToken = function(cb) { 75 | const user = { 76 | _id: this._id, 77 | nonce: this.nonce.verifyEmail, 78 | }; 79 | const token = jwt.sign(user, configs.jwt.verifyEmail.secret, { 80 | expiresIn: configs.jwt.verifyEmail.expiresIn, 81 | }); 82 | return token; 83 | }; 84 | 85 | UserSchema.methods.toResetPasswordToken = function(cb) { 86 | const user = { 87 | _id: this._id, 88 | nonce: this.nonce.resetPassword, 89 | }; 90 | const token = jwt.sign(user, configs.jwt.resetPassword.secret, { 91 | expiresIn: configs.jwt.resetPassword.expiresIn, 92 | }); 93 | return token; 94 | }; 95 | 96 | UserSchema.methods.toAuthenticationToken = function(cb) { 97 | const user = { 98 | _id: this._id, 99 | name: this.name, 100 | email: this.email, 101 | }; 102 | const token = jwt.sign(user, configs.jwt.authentication.secret, { 103 | expiresIn: configs.jwt.authentication.expiresIn, 104 | }); 105 | return token; 106 | }; 107 | 108 | UserSchema.methods.toJSON = function() { 109 | let obj = this.toObject(); 110 | delete obj.password; 111 | return obj; 112 | }; 113 | 114 | let User = mongoose.model('User', UserSchema); 115 | export default User; 116 | -------------------------------------------------------------------------------- /src/server/models/plugins/paginate.js: -------------------------------------------------------------------------------- 1 | /* 2 | * refs: 3 | * - plugin: <http://mongoosejs.com/docs/plugins.html> 4 | * - pagination: <http://stackoverflow.com/questions/5539955/how-to-paginate-with-mongoose-in-node-js> 5 | * 6 | * example usage: 7 | * ``` 8 | * import paginatePlugin from './plugins/paginate'; 9 | * YourSchema.plugin(paginatePlugin); 10 | * ``` 11 | * 12 | * ``` 13 | * someListController(req, res) { 14 | * YourSchema.paginate({page: req.query.page}, (err, page) => { 15 | * YourSchema 16 | * .find({}) 17 | * .limit(page.limit) 18 | * .skip(page.skip) 19 | * .exec((err, yourEntries) => { 20 | * res.json({ 21 | * yourEntries: yourEntries, 22 | * page: page, 23 | * }); 24 | * }); 25 | * }); 26 | * } 27 | * ``` 28 | */ 29 | 30 | function getOptions(customOpts) { 31 | let opts = {}; 32 | 33 | opts.condition = customOpts.condition || {}; 34 | opts.perPage = Number(customOpts.perPage) || 20; 35 | opts.firstPage = Number(customOpts.firstPage) || 1; 36 | opts.page = Number(customOpts.page) || 1; 37 | 38 | return opts; 39 | } 40 | 41 | export function recordCountToPageObject(count, customOpts) { 42 | let opts = getOptions(customOpts); 43 | 44 | let totalPage = Math.max(Math.ceil(count / opts.perPage), 1); 45 | let lastPage = opts.firstPage + totalPage - 1; 46 | 47 | if (opts.page < opts.firstPage) { 48 | opts.page = opts.firstPage; 49 | } else if (lastPage < opts.page) { 50 | opts.page = lastPage; 51 | } 52 | 53 | return { 54 | skip: opts.perPage * (opts.page - opts.firstPage), 55 | limit: opts.perPage, 56 | first: opts.firstPage, 57 | current: opts.page, 58 | last: lastPage, 59 | total: totalPage, 60 | }; 61 | }; 62 | 63 | export function recordsOfPage({ records, page, sort }) { 64 | if (sort) { 65 | return records 66 | .sort(sort) 67 | .slice(page.skip, page.skip + page.limit); 68 | } 69 | return records 70 | .slice(page.skip, page.skip + page.limit); 71 | }; 72 | 73 | export default (schema, options) => { 74 | schema.statics.paginate = function paginate(customOpts, cb) { 75 | let opts = getOptions(customOpts); 76 | 77 | this 78 | .count(opts.condition) 79 | .exec((err, count) => { 80 | let page = recordCountToPageObject(count, opts); 81 | 82 | cb(err, page); 83 | }); 84 | }; 85 | }; 86 | -------------------------------------------------------------------------------- /src/server/pm2Entry.js: -------------------------------------------------------------------------------- 1 | // ref: <http://pm2.keymetrics.io/docs/usage/use-pm2-with-cloud-providers/> 2 | var pm2 = require('pm2'); 3 | 4 | var instances = process.env.WEB_CONCURRENCY || -1; // Set by Heroku or -1 to scale to max cpu core -1 5 | var maxMemory = process.env.WEB_MEMORY || 512; // " " " 6 | 7 | pm2.connect(function() { 8 | pm2.start({ 9 | script: './build/server/server.js', 10 | name: 'production-app', // ----> THESE ATTRIBUTES ARE OPTIONAL: 11 | exec_mode: 'cluster', // ----> https://github.com/Unitech/PM2/blob/master/ADVANCED_README.md#schema 12 | instances: instances, 13 | max_memory_restart: maxMemory + 'M', // Auto restart if process taking more than XXmo 14 | env: { // If needed declare some environment variables 15 | NODE_ENV: 'production', 16 | AWESOME_SERVICE_API_TOKEN: 'xxx', 17 | }, 18 | }, function(err) { 19 | if (err) { 20 | return console.error( 21 | 'Error while launching applications', 22 | err.stack || err 23 | ); 24 | } 25 | console.log('PM2 and application has been succesfully started'); 26 | 27 | // Display logs in standard output 28 | pm2.launchBus(function(err, bus) { 29 | if (err) { 30 | throw err; 31 | } 32 | console.log('[PM2] Log streaming started'); 33 | 34 | bus.on('log:out', function(packet) { 35 | console.log('[App:%s] %s', packet.process.name, packet.data); 36 | }); 37 | 38 | bus.on('log:err', function(packet) { 39 | console.error('[App:%s][Err] %s', packet.process.name, packet.data); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/server/routes/index.js: -------------------------------------------------------------------------------- 1 | import apiRoutes from './api'; 2 | import socialAuthRoutes from './socialAuth'; 3 | import ssrFetchStateRoutes from './ssrFetchState'; 4 | import ssrRoutes from './ssr'; 5 | 6 | export default ({ app }) => { 7 | apiRoutes({ app }); 8 | socialAuthRoutes({ app }); 9 | ssrFetchStateRoutes({ app }); 10 | ssrRoutes({ app }); 11 | }; 12 | -------------------------------------------------------------------------------- /src/server/routes/socialAuth.js: -------------------------------------------------------------------------------- 1 | import passportAuth from '../middlewares/passportAuth'; 2 | import socialAuthController from '../controllers/socialAuth'; 3 | import userController from '../controllers/user'; 4 | import configs from '../../../configs/project/server'; 5 | 6 | export default ({ app }) => { 7 | // facebook 8 | if (configs.passportStrategy.facebook) { 9 | app.get('/auth/facebook', socialAuthController.initFacebook); 10 | app.get('/auth/facebook/callback', 11 | passportAuth('facebook'), 12 | userController.socialLogin 13 | ); 14 | } else { 15 | app.get('/auth/facebook', socialAuthController.setupError); 16 | } 17 | // linkedin 18 | if (configs.passportStrategy.linkedin) { 19 | app.get('/auth/linkedin', socialAuthController.initLinkedin); 20 | app.get('/auth/linkedin/callback', 21 | passportAuth('linkedin'), 22 | userController.socialLogin 23 | ); 24 | } else { 25 | app.get('/auth/linkedin', socialAuthController.setupError); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/server/routes/ssr.js: -------------------------------------------------------------------------------- 1 | import reactController from '../controllers/react'; 2 | 3 | export default ({ app }) => { 4 | app.get('/*', reactController.render); 5 | }; 6 | -------------------------------------------------------------------------------- /src/server/routes/ssrFetchState.js: -------------------------------------------------------------------------------- 1 | import ssrFetchStateController from '../controllers/ssrFetchState'; 2 | 3 | export default ({ app }) => { 4 | app.use('/*', ssrFetchStateController.user, ssrFetchStateController.intl); 5 | app.get('/todo', ssrFetchStateController.todo); 6 | }; 7 | -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | import appPromise from './app'; 2 | import getPort from './utils/getPort'; 3 | 4 | appPromise 5 | .catch((err) => { 6 | console.log(err.stack); 7 | }) 8 | .then((app) => { 9 | // launch server 10 | const port = getPort(); 11 | app.listen(port, (err) => { 12 | if (err) { 13 | throw err; 14 | } 15 | if (app.get('env') !== 'test') { 16 | console.log('Listening at port', port); 17 | } 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/server/utils/env.js: -------------------------------------------------------------------------------- 1 | const env = process.env.NODE_ENV = process.env.NODE_ENV || 'production'; 2 | export default env; 3 | -------------------------------------------------------------------------------- /src/server/utils/filterAttribute.js: -------------------------------------------------------------------------------- 1 | export default (obj, allowedAttributes) => { 2 | let resultObj = {}; 3 | Object 4 | .keys(obj) 5 | .filter((attribute) => allowedAttributes.indexOf(attribute) >= 0) 6 | .forEach((attribute) => { 7 | resultObj[attribute] = obj[attribute]; 8 | }); 9 | return resultObj; 10 | }; 11 | -------------------------------------------------------------------------------- /src/server/utils/getPort.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const args = process.argv.slice(2); 3 | const portIndex = args.indexOf('-p'); 4 | let argPort = null; 5 | if (portIndex >= 0) { 6 | argPort = parseInt(args[portIndex + 1]); 7 | } 8 | return process.env.PORT || argPort || 3000; 9 | }; 10 | -------------------------------------------------------------------------------- /src/server/utils/tokenToURL.js: -------------------------------------------------------------------------------- 1 | import configs from '../../../configs/project/server'; 2 | 3 | export default (baseURL, token) => ( 4 | `${configs.host[process.env.NODE_ENV]}${baseURL}?token=${token}` 5 | ); 6 | -------------------------------------------------------------------------------- /src/server/utils/validateErrorObject.js: -------------------------------------------------------------------------------- 1 | import isString from 'lodash/isString'; 2 | 3 | /** 4 | * @returns {boolean} isPassed 5 | */ 6 | let validateErrorObject = (nestedErrors) => { 7 | if (isString(nestedErrors)) { 8 | return false; 9 | } else { 10 | let keys = Object.keys(nestedErrors); 11 | if (keys.length === 0) { 12 | return true; 13 | } else { 14 | let isPass = keys.every((key) => { 15 | let nestedError = nestedErrors[key]; 16 | let isPass = validateErrorObject(nestedError); 17 | return isPass; 18 | }); 19 | return isPass; 20 | } 21 | } 22 | }; 23 | 24 | export default validateErrorObject; 25 | -------------------------------------------------------------------------------- /src/server/webpackIsomorphicToolsInjector.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import WebpackIsomorphicTools from 'webpack-isomorphic-tools'; 3 | import webpackIsomorphicToolsConfig 4 | from '../../configs/project/webpack-isomorphic-tools-configuration'; 5 | 6 | const injector = new Promise((resolve, reject) => { 7 | // project root, sync with `webpack.config.dev.js` and `webpack.config.prod.js` 8 | let projectBasePath = path.resolve(__dirname, '../'); 9 | global.__webpackIsomorphicTools__ = 10 | new WebpackIsomorphicTools(webpackIsomorphicToolsConfig) 11 | .development(process.env.NODE_ENV === 'development') 12 | .server(projectBasePath, resolve); 13 | }); 14 | 15 | export default injector; 16 | --------------------------------------------------------------------------------