├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── app ├── actions │ ├── index.js │ └── user.js ├── api │ └── .gitkeep ├── app.global.css ├── app.html ├── app.icns ├── app.rich-editor.css ├── components │ ├── A │ │ ├── index.js │ │ └── styles.css │ ├── Button │ │ ├── index.js │ │ └── styles.css │ ├── H1 │ │ ├── index.js │ │ └── styles.css │ ├── Home.css │ ├── Home.js │ ├── Modal │ │ ├── index.js │ │ └── styles.css │ ├── P │ │ ├── index.js │ │ └── styles.css │ ├── Post │ │ ├── index.js │ │ └── styles.css │ ├── Posts │ │ ├── index.js │ │ └── styles.css │ ├── Select │ │ ├── index.js │ │ └── styles.css │ ├── TagInput │ │ ├── index.js │ │ └── styles.css │ ├── TopNav │ │ ├── index.js │ │ └── styles.css │ ├── VerticalAlign │ │ ├── index.js │ │ └── styles.css │ ├── Wrap │ │ ├── index.js │ │ └── styles.css │ └── ZeroState │ │ ├── index.js │ │ └── styles.css ├── containers │ ├── About │ │ ├── index.js │ │ └── styles.css │ ├── App.js │ ├── DevTools.js │ ├── HomePage │ │ ├── index.js │ │ └── styles.css │ └── New │ │ ├── index.js │ │ └── styles.css ├── index.js ├── reducers │ ├── index.js │ └── user.js ├── routes.js ├── store │ ├── configureStore.development.js │ ├── configureStore.js │ └── configureStore.production.js └── utils │ ├── .gitkeep │ ├── create_post.js │ ├── get_posts.js │ ├── get_user.js │ └── settings.js ├── erb-logo.png ├── icon.icns ├── icon.ico ├── icon.png ├── main.development.js ├── package.js ├── package.json ├── server.js ├── test ├── .eslintrc ├── actions │ └── counter.spec.js ├── components │ └── Counter.spec.js ├── containers │ └── CounterPage.spec.js ├── e2e.js ├── example.js ├── reducers │ └── counter.spec.js └── setup.js ├── webpack.config.base.js ├── webpack.config.development.js ├── webpack.config.electron.js ├── webpack.config.node.js └── webpack.config.production.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["add-module-exports"], 4 | "env": { 5 | "production": { 6 | "presets": ["react-optimize"], 7 | "plugins": [ 8 | "babel-plugin-transform-remove-console", 9 | "babel-plugin-transform-remove-debugger", 10 | "babel-plugin-dev-expression" 11 | ] 12 | }, 13 | "development": { 14 | "presets": ["react-hmre"] 15 | }, 16 | "test": { 17 | "plugins": [ 18 | ["webpack-loaders", { "config": "webpack.config.node.js", "verbose": false }] 19 | ] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.{json,js,jsx,html,css}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [.eslintrc] 15 | indent_style = space 16 | indent_size = 2 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | main.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "browser": true, 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "rules": { 10 | "consistent-return": 0, 11 | "comma-dangle": 0, 12 | "no-use-before-define": 0, 13 | "import/no-unresolved": [2, { ignore: ['electron'] }], 14 | "react/jsx-no-bind": 0, 15 | "react/prefer-stateless-function": 0 16 | }, 17 | "plugins": [ 18 | "import", 19 | "react" 20 | ], 21 | "settings": { 22 | "import/resolver": "webpack" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | 29 | # OSX 30 | .DS_Store 31 | 32 | # App packaged 33 | .env 34 | dist 35 | release 36 | main.js 37 | main.js.map 38 | app/settings.json -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "5" 6 | - "6" 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | addons: 13 | apt: 14 | sources: 15 | - ubuntu-toolchain-r-test 16 | packages: 17 | - g++-4.8 18 | 19 | install: 20 | - export CXX="g++-4.8" 21 | - npm install 22 | - "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16" 23 | 24 | before_script: 25 | - export DISPLAY=:99.0 26 | - sh -e /etc/init.d/xvfb start & 27 | - sleep 3 28 | 29 | script: 30 | - npm run lint 31 | - npm run test 32 | - npm run build 33 | - npm run test-e2e 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.10.0 (2016.4.18) 2 | 3 | #### Improvements 4 | 5 | - **Use Babel in main process with Webpack build:** [#201](https://github.com/chentsulin/electron-react-boilerplate/pull/201) 6 | - **Change targets to built-in support by webpack:** [#197](https://github.com/chentsulin/electron-react-boilerplate/pull/197) 7 | - **use es2015 syntax for webpack configs:** [#195](https://github.com/chentsulin/electron-react-boilerplate/pull/195) 8 | - **Open application when webcontent is loaded:** [#192](https://github.com/chentsulin/electron-react-boilerplate/pull/192) 9 | - **Upgraded dependencies** 10 | 11 | #### Bug fixed 12 | 13 | - **Fix `npm list electron-prebuilt` in package.js:** [#188](https://github.com/chentsulin/electron-react-boilerplate/pull/188) 14 | 15 | 16 | # 0.9.0 (2016.3.23) 17 | 18 | #### Improvements 19 | 20 | - **Added [redux-logger](https://github.com/fcomb/redux-logger)** 21 | - **Upgraded [react-router-redux](https://github.com/reactjs/react-router-redux) to v4** 22 | - **Upgraded dependencies** 23 | - **Added `npm run dev` command:** [#162](https://github.com/chentsulin/electron-react-boilerplate/pull/162) 24 | - **electron to v0.37.2** 25 | 26 | #### Breaking Changes 27 | 28 | - **css module as default:** [#154](https://github.com/chentsulin/electron-react-boilerplate/pull/154). 29 | - **set default NODE_ENV to production:** [#140](https://github.com/chentsulin/electron-react-boilerplate/issues/140) 30 | 31 | 32 | # 0.8.0 (2016.2.17) 33 | 34 | #### Bug fixed 35 | 36 | - **Fix lint errors** 37 | - **Fix Webpack publicPath for production builds**: [#119](https://github.com/chentsulin/electron-react-boilerplate/issues/119). 38 | - **package script now chooses correct OS icon extension** 39 | 40 | #### Improvements 41 | 42 | - **babel 6** 43 | - **Upgrade Dependencies** 44 | - **Enable CSS source maps** 45 | - **Add json-loader**: [#128](https://github.com/chentsulin/electron-react-boilerplate/issues/128). 46 | - **react-router 2.0 and react-router-redux 3.0** 47 | 48 | 49 | # 0.7.1 (2015.12.27) 50 | 51 | #### Bug fixed 52 | 53 | - **Fixed npm script on windows 10:** [#103](https://github.com/chentsulin/electron-react-boilerplate/issues/103). 54 | - **history and react-router version bump**: [#109](https://github.com/chentsulin/electron-react-boilerplate/issues/109), [#110](https://github.com/chentsulin/electron-react-boilerplate/pull/110). 55 | 56 | #### Improvements 57 | 58 | - **electron 0.36** 59 | 60 | 61 | 62 | # 0.7.0 (2015.12.16) 63 | 64 | #### Bug fixed 65 | 66 | - **Fixed process.env.NODE_ENV variable in webpack:** [#74](https://github.com/chentsulin/electron-react-boilerplate/pull/74). 67 | - **add missing object-assign**: [#76](https://github.com/chentsulin/electron-react-boilerplate/pull/76). 68 | - **packaging in npm@3:** [#77](https://github.com/chentsulin/electron-react-boilerplate/pull/77). 69 | - **compatibility in windows:** [#100](https://github.com/chentsulin/electron-react-boilerplate/pull/100). 70 | - **disable chrome debugger in production env:** [#102](https://github.com/chentsulin/electron-react-boilerplate/pull/102). 71 | 72 | #### Improvements 73 | 74 | - **redux** 75 | - **css-modules** 76 | - **upgrade to react-router 1.x** 77 | - **unit tests** 78 | - **e2e tests** 79 | - **travis-ci** 80 | - **upgrade to electron 0.35.x** 81 | - **use es2015** 82 | - **check dev engine for node and npm** 83 | 84 | 85 | # 0.6.5 (2015.11.7) 86 | 87 | #### Improvements 88 | 89 | - **Bump style-loader to 0.13** 90 | - **Bump css-loader to 0.22** 91 | 92 | 93 | # 0.6.4 (2015.10.27) 94 | 95 | #### Improvements 96 | 97 | - **Bump electron-debug to 0.3** 98 | 99 | 100 | # 0.6.3 (2015.10.26) 101 | 102 | #### Improvements 103 | 104 | - **Initialize ExtractTextPlugin once:** [#64](https://github.com/chentsulin/electron-react-boilerplate/issues/64). 105 | 106 | 107 | # 0.6.2 (2015.10.18) 108 | 109 | #### Bug fixed 110 | 111 | - **Babel plugins production env not be set properly:** [#57](https://github.com/chentsulin/electron-react-boilerplate/issues/57). 112 | 113 | 114 | # 0.6.1 (2015.10.17) 115 | 116 | #### Improvements 117 | 118 | - **Bump electron to v0.34.0** 119 | 120 | 121 | # 0.6.0 (2015.10.16) 122 | 123 | #### Breaking Changes 124 | 125 | - **From react-hot-loader to react-transform** 126 | 127 | 128 | # 0.5.2 (2015.10.15) 129 | 130 | #### Improvements 131 | 132 | - **Run tests with babel-register:** [#29](https://github.com/chentsulin/electron-react-boilerplate/issues/29). 133 | 134 | 135 | # 0.5.1 (2015.10.12) 136 | 137 | #### Bug fixed 138 | 139 | - **Fix #51:** use `path.join(__dirname` instead of `./`. 140 | 141 | 142 | # 0.5.0 (2015.10.11) 143 | 144 | #### Improvements 145 | 146 | - **Simplify webpack config** see [#50](https://github.com/chentsulin/electron-react-boilerplate/pull/50). 147 | 148 | #### Breaking Changes 149 | 150 | - **webpack configs** 151 | - **port changed:** changed default port from 2992 to 3000. 152 | - **npm scripts:** remove `start-dev` and `dev-server`. rename `hot-dev-server` to `hot-server`. 153 | 154 | 155 | # 0.4.3 (2015.9.22) 156 | 157 | #### Bug fixed 158 | 159 | - **Fix #45 zeromq crash:** bump version of `electron-prebuilt`. 160 | 161 | 162 | # 0.4.2 (2015.9.15) 163 | 164 | #### Bug fixed 165 | 166 | - **run start-hot breaks chrome refresh(CTRL+R) (#42)**: bump `electron-debug` to `0.2.1` 167 | 168 | 169 | # 0.4.1 (2015.9.11) 170 | 171 | #### Improvements 172 | 173 | - **use electron-prebuilt version for packaging (#33)** 174 | 175 | 176 | # 0.4.0 (2015.9.5) 177 | 178 | #### Improvements 179 | 180 | - **update dependencies** 181 | 182 | 183 | # 0.3.0 (2015.8.31) 184 | 185 | #### Improvements 186 | 187 | - **eslint-config-airbnb** 188 | 189 | 190 | # 0.2.10 (2015.8.27) 191 | 192 | #### Features 193 | 194 | - **custom placeholder icon** 195 | 196 | #### Improvements 197 | 198 | - **electron-renderer as target:** via [webpack-target-electron-renderer](https://github.com/chentsulin/webpack-target-electron-renderer) 199 | 200 | 201 | # 0.2.9 (2015.8.18) 202 | 203 | #### Bug fixed 204 | 205 | - **Fix hot-reload** 206 | 207 | 208 | # 0.2.8 (2015.8.13) 209 | 210 | #### Improvements 211 | 212 | - **bump electron-debug** 213 | - **babelrc** 214 | - **organize webpack scripts** 215 | 216 | 217 | # 0.2.7 (2015.7.9) 218 | 219 | #### Bug fixed 220 | 221 | - **defaultProps:** fix typos. 222 | 223 | 224 | # 0.2.6 (2015.7.3) 225 | 226 | #### Features 227 | 228 | - **menu** 229 | 230 | #### Bug fixed 231 | 232 | - **package.js:** include webpack build. 233 | 234 | 235 | # 0.2.5 (2015.7.1) 236 | 237 | #### Features 238 | 239 | - **NPM Script:** support multi-platform 240 | - **package:** `--all` option 241 | 242 | 243 | # 0.2.4 (2015.6.9) 244 | 245 | #### Bug fixed 246 | 247 | - **Eslint:** typo, [#17](https://github.com/chentsulin/electron-react-boilerplate/issues/17) and improve `.eslintrc` 248 | 249 | 250 | # 0.2.3 (2015.6.3) 251 | 252 | #### Features 253 | 254 | - **Package Version:** use latest release electron version as default 255 | - **Ignore Large peerDependencies** 256 | 257 | #### Bug fixed 258 | 259 | - **Npm Script:** typo, [#6](https://github.com/chentsulin/electron-react-boilerplate/pull/6) 260 | - **Missing css:** [#7](https://github.com/chentsulin/electron-react-boilerplate/pull/7) 261 | 262 | 263 | # 0.2.2 (2015.6.2) 264 | 265 | #### Features 266 | 267 | - **electron-debug** 268 | 269 | #### Bug fixed 270 | 271 | - **Webpack:** add `.json` and `.node` to extensions for imitating node require. 272 | - **Webpack:** set `node_modules` to externals for native module support. 273 | 274 | 275 | # 0.2.1 (2015.5.30) 276 | 277 | #### Bug fixed 278 | 279 | - **Webpack:** #1, change build target to `atom`. 280 | 281 | 282 | # 0.2.0 (2015.5.30) 283 | 284 | #### Features 285 | 286 | - **Ignore:** `test`, `tools`, `release` folder and devDependencies in `package.json`. 287 | - **Support asar** 288 | - **Support icon** 289 | 290 | 291 | # 0.1.0 (2015.5.27) 292 | 293 | #### Features 294 | 295 | - **Webpack:** babel, react-hot, ... 296 | - **Flux:** actions, api, components, containers, stores.. 297 | - **Package:** darwin (osx), linux and win32 (windows) platform. 298 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # s'more 2 | 3 | ### Development 4 | 5 | 1. `npm install` 6 | 2. Talk to Gavin about Medium api keys. 7 | 3. Make file in `/app` called `settings.json` add in `{"user": {}}` 8 | 4. `npm run dev` 9 | 5. You should see screen saying you need to sign in. -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as routing } from 'react-router-redux'; 3 | import user from './user'; 4 | 5 | const rootReducer = combineReducers({ 6 | user, 7 | routing 8 | }); 9 | 10 | export default rootReducer; -------------------------------------------------------------------------------- /app/actions/user.js: -------------------------------------------------------------------------------- 1 | export const SET_USER = 'SET_USER'; 2 | import get from '../utils/get_user'; 3 | 4 | export function set(user) { 5 | return { 6 | type: SET_USER, 7 | user: user 8 | }; 9 | } 10 | 11 | export function getFromMedium() { 12 | return dispatch => { 13 | get((user) => { 14 | dispatch(set(user)); 15 | }); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /app/api/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/app/api/.gitkeep -------------------------------------------------------------------------------- /app/app.global.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | box-sizing: border-box; 5 | position: relative; 6 | } 7 | 8 | body { 9 | position: relative; 10 | font-family: Arial, Helvetica, Helvetica Neue; 11 | min-height: 100vh; 12 | } 13 | 14 | h2 { 15 | margin: 0; 16 | font-size: 2.25rem; 17 | font-weight: bold; 18 | letter-spacing: -.025em; 19 | color: #fff; 20 | } 21 | 22 | p { 23 | font-size: 24px; 24 | } 25 | 26 | #root { 27 | min-height: 100vh; 28 | } 29 | 30 | #root [data-reactroot] { 31 | min-height: 100vh; 32 | } 33 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | s'more - Desktop Medium Writer 6 | 7 | 17 | 18 | 19 |
20 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/app/app.icns -------------------------------------------------------------------------------- /app/app.rich-editor.css: -------------------------------------------------------------------------------- 1 | .public-DraftEditorPlaceholder-root { 2 | position: absolute; 3 | color: rgba(0,0,0,0.4); 4 | } 5 | 6 | .RichEditor-editor h1 { 7 | color: rgba(0,0,0,.8); 8 | font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; 9 | font-weight: 700; 10 | font-style: normal; 11 | font-size: 36px; 12 | margin-left: -2.25px; 13 | line-height: 1.15; 14 | letter-spacing: -.02em; 15 | } 16 | 17 | .RichEditor-editor h2 { 18 | color: rgba(0,0,0,.44); 19 | font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; 20 | letter-spacing: -.02em; 21 | font-weight: 300; 22 | font-style: normal; 23 | font-size: 28px; 24 | margin-left: -1.75px; 25 | line-height: 1.22; 26 | letter-spacing: -.022em; 27 | 28 | margin-top: 65px; 29 | } 30 | 31 | .RichEditor-editor h1 ~ h2:first-of-type, h2:first-of-type { 32 | margin-top: 1px; 33 | } 34 | 35 | .RichEditor-editor div[data-block="true"] { 36 | margin-top: 29px; 37 | } 38 | 39 | .RichEditor-editor div[data-block="true"], .public-DraftEditorPlaceholder-root, .public-DraftStyleDefault-unorderedListItem, .public-DraftStyleDefault-orderedListItem { 40 | font-family: Georgia,Cambria,"Times New Roman",Times,serif; 41 | 42 | font-weight: 400; 43 | font-style: normal; 44 | font-size: 21px; 45 | line-height: 1.58; 46 | letter-spacing: -.003em; 47 | } 48 | 49 | .public-DraftStyleDefault-unorderedListItem, .public-DraftStyleDefault-orderedListItem { 50 | margin-left: 30px; 51 | } 52 | 53 | .RichEditor-editor h1 ~ div[data-block="true"]:first-of-type { 54 | margin-top: 12px; 55 | } -------------------------------------------------------------------------------- /app/components/A/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | import styles from './styles.css'; 4 | 5 | const A = (props) => ; 6 | 7 | export default A; -------------------------------------------------------------------------------- /app/components/A/styles.css: -------------------------------------------------------------------------------- 1 | .a { 2 | color: #3ba2e0; 3 | font-weight: 400; 4 | text-decoration: none; 5 | } 6 | 7 | .a:hover { 8 | color: #2f8bc1; 9 | } -------------------------------------------------------------------------------- /app/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | function Button(props) { 6 | return ( 7 | 9 | ); 10 | } 11 | 12 | export default Button; 13 | -------------------------------------------------------------------------------- /app/components/Button/styles.css: -------------------------------------------------------------------------------- 1 | .button { 2 | display: block; 3 | padding: 15px; 4 | color: #fff; 5 | border: 1px solid #fff; 6 | border-radius: 30px; 7 | background-color: #3ba2e0; 8 | outline: none; 9 | margin-bottom: 15px; 10 | width: 100%; 11 | max-width: 200px; 12 | cursor: pointer; 13 | } 14 | 15 | .button:hover { 16 | background-color: #2f8bc1; 17 | } 18 | 19 | .button:disabled { 20 | background-color: rgba(0,0,0,0.1); 21 | cursor: not-allowed; 22 | } 23 | 24 | .button--center { 25 | margin: 0 auto; 26 | margin-bottom: 15px; 27 | } -------------------------------------------------------------------------------- /app/components/H1/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const H1 = (props) =>

; 5 | 6 | module.exports = H1; -------------------------------------------------------------------------------- /app/components/H1/styles.css: -------------------------------------------------------------------------------- 1 | .h1 { 2 | color: rgba(0,0,0,.8); 3 | font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; 4 | font-weight: 700; 5 | font-style: normal; 6 | font-size: 32px; 7 | padding-bottom: 15px; 8 | margin-left: -2.25px; 9 | line-height: 1.15; 10 | letter-spacing: -.02em; 11 | } -------------------------------------------------------------------------------- /app/components/Home.css: -------------------------------------------------------------------------------- 1 | .container { 2 | position: absolute; 3 | top: 30%; 4 | left: 10px; 5 | text-align: center; 6 | } 7 | 8 | .container h2 { 9 | font-size: 5rem; 10 | } 11 | 12 | .container a { 13 | font-size: 1.4rem; 14 | } 15 | -------------------------------------------------------------------------------- /app/components/Home.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { Link } from 'react-router'; 3 | import styles from './Home.css'; 4 | 5 | export default class Home extends Component { 6 | render() { 7 | return ( 8 |
9 |
10 |

Home

11 | to Counter 12 |
13 |
14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css' 3 | 4 | const Modal = (props) => { 5 | let modalClass = `${styles.modal}`; 6 | let backDropClass = `${styles.modal_backdrop}`; 7 | if (props.show) modalClass = `${modalClass} ${styles.modal__active}`; 8 | if (props.show) backDropClass = `${backDropClass} ${styles.modal_backdrop__active}`; 9 | return ( 10 |
11 |
12 |
13 | {props.children} 14 |
15 |
16 | ); 17 | } 18 | 19 | module.exports = Modal; -------------------------------------------------------------------------------- /app/components/Modal/styles.css: -------------------------------------------------------------------------------- 1 | .modal { 2 | position: fixed; 3 | bottom: 100%; 4 | left: 50%; 5 | 6 | width: 700px; 7 | min-height: 50vh; 8 | max-height: 100vh; 9 | overflow: scroll; 10 | 11 | padding: 15px; 12 | background-color: white; 13 | box-shadow: none; 14 | 15 | transition: all 0.3s; 16 | transform: translate(-50%, 0%); 17 | } 18 | 19 | .modal_backdrop { 20 | position: fixed; 21 | top: 0; 22 | left: 0; 23 | width: 100vw; 24 | height: 100vh; 25 | display: none; 26 | } 27 | 28 | .modal_backdrop__active { 29 | display: block; 30 | } 31 | 32 | .modal__active { 33 | transform: translate(-50%, 100%); 34 | box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2); 35 | } -------------------------------------------------------------------------------- /app/components/P/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const P = (props) =>

5 | 6 | module.exports = P; -------------------------------------------------------------------------------- /app/components/P/styles.css: -------------------------------------------------------------------------------- 1 | .P { 2 | font-family: Georgia,Cambria,"Times New Roman",Times,serif; 3 | 4 | font-weight: 400; 5 | font-style: normal; 6 | font-size: 18px; 7 | line-height: 1.58; 8 | letter-spacing: -.003em; 9 | } -------------------------------------------------------------------------------- /app/components/Post/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const Post = (props) => { 5 | let {post} = props; 6 | return ( 7 | 8 |

{post.title}

9 | {(() => { 10 | if (!post.virtuals.previewImage.imageId) return; 11 | return 12 | })()} 13 |

{post.virtuals.subtitle}

14 |
15 | ); 16 | } 17 | 18 | export default Post; -------------------------------------------------------------------------------- /app/components/Post/styles.css: -------------------------------------------------------------------------------- 1 | .post { 2 | padding: 15px; 3 | background: #fff; 4 | box-shadow: 0 1px 4px rgba(0,0,0,.04); 5 | border: 1px solid rgba(0,0,0,.09); 6 | margin: 15px 0; 7 | display: block; 8 | text-decoration: none; 9 | } 10 | 11 | .post_header { 12 | font-size: 31px; 13 | margin-left: -1.94px; 14 | line-height: 1.12; 15 | letter-spacing: -.022em; 16 | font-family: medium-content-sans-serif-font,"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; 17 | font-weight: 700; 18 | font-style: normal; 19 | color: rgba(0,0,0,.8) 20 | } 21 | 22 | .post_image { 23 | max-height: 240px; 24 | } 25 | 26 | .post_snippet { 27 | font-family: medium-content-sans-serif-font,"Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; 28 | 29 | font-weight: 400; 30 | font-style: normal; 31 | font-size: 18px; 32 | line-height: 1.58; 33 | letter-spacing: -.003em; 34 | color: rgba(0,0,0,.44); 35 | } -------------------------------------------------------------------------------- /app/components/Posts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Post from '../Post'; 3 | import ZeroState from '../ZeroState'; 4 | import A from '../A'; 5 | import H1 from '../H1'; 6 | 7 | const Posts = (props) => { 8 | let posts_map = []; 9 | for (let key in props.posts) { 10 | console.log(key); 11 | posts_map.push(props.posts[key]); 12 | } 13 | if (!posts_map || !posts_map.length) { 14 | return ( 15 | 16 | You don't have any stories yet, why not write one? 17 | 18 | ); 19 | } 20 | let posts = posts_map.map((post) => { 21 | console.log(post); 22 | return 23 | }); 24 | return ( 25 |
26 |

Your Posts

27 | {posts} 28 |
29 | ); 30 | } 31 | 32 | export default Posts; -------------------------------------------------------------------------------- /app/components/Posts/styles.css: -------------------------------------------------------------------------------- 1 | .posts { 2 | } -------------------------------------------------------------------------------- /app/components/Select/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const Select = (props) => ; 5 | 6 | module.exports = Select; -------------------------------------------------------------------------------- /app/components/Select/styles.css: -------------------------------------------------------------------------------- 1 | .Select { 2 | background: transparent; 3 | border: none; 4 | border-radius: 0%; 5 | border-bottom: 1px solid rgba(0,0,0,0.8); 6 | outline: none; 7 | color: rgba(0,0,0,0.6); 8 | 9 | -webkit-appearance: none; 10 | font-family: Georgia,Cambria,"Times New Roman",Times,serif; 11 | 12 | font-weight: 400; 13 | font-style: normal; 14 | font-size: 18px; 15 | line-height: 1.58; 16 | margin: 0 5px; 17 | } -------------------------------------------------------------------------------- /app/components/TagInput/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import styles from './styles.css'; 3 | 4 | import Tags from 'react-tagsinput' 5 | 6 | import 'react-tagsinput/react-tagsinput.css' // If using WebPack and style-loader. 7 | 8 | export default class TagInput extends Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | tags: [] 13 | } 14 | } 15 | handleChange(tags) { 16 | this.setState({tags}) 17 | } 18 | render() { 19 | return ( 20 |
21 | this.handleChange(tags)} 24 | tagProps={{className: styles.tag}} 25 | inputProps={{className: styles.input}} 26 | /> 27 |
28 | ); 29 | } 30 | } -------------------------------------------------------------------------------- /app/components/TagInput/styles.css: -------------------------------------------------------------------------------- 1 | .tags { 2 | display: inline-block; 3 | margin: 0 5px; 4 | margin-top: 15px; 5 | } 6 | 7 | .tag { 8 | display: inline-block; 9 | font-family: Georgia,Cambria,"Times New Roman",Times,serif; 10 | 11 | font-weight: 400; 12 | font-style: normal; 13 | font-size: 18px; 14 | line-height: 1.58; 15 | padding: 0 5px; 16 | padding-right: 15px; 17 | border-bottom: 1px solid rgba(0,0,0,0.8); 18 | margin: 0 2px; 19 | position: relative; 20 | } 21 | 22 | .tag a { 23 | position: absolute; 24 | right: 0; 25 | color: rgba(0,0,0,0.5); 26 | transform: rotate(-45deg); 27 | } 28 | 29 | .tag a:after { 30 | content: '+'; 31 | } 32 | 33 | .input { 34 | border: none; 35 | border-bottom: 1px solid rgba(0,0,0,0.8); 36 | outline: none; 37 | color: rgba(0,0,0,0.6); 38 | 39 | font-family: Georgia,Cambria,"Times New Roman",Times,serif; 40 | 41 | font-weight: 400; 42 | font-style: normal; 43 | font-size: 18px; 44 | line-height: 1.58; 45 | margin: 0 5px; 46 | } -------------------------------------------------------------------------------- /app/components/TopNav/index.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import styles from './styles.css'; 3 | import Wrap from '../Wrap'; 4 | import A from '../A'; 5 | 6 | class TopNav extends Component { 7 | constructor(props) { 8 | super(props); 9 | this.state = { 10 | nav_open: false 11 | }; 12 | } 13 | toggleNav() { 14 | this.setState({nav_open: !this.state.nav_open}); 15 | } 16 | render() { 17 | let button_class = ''; 18 | let link_class = ''; 19 | if (this.state.nav_open) { 20 | button_class = styles.topnav_button__active; 21 | link_class = styles.topnav_links__active; 22 | } 23 | return ( 24 |
25 | 26 |
27 | Home 28 | New Story 29 |
30 |
31 | ); 32 | } 33 | } 34 | 35 | export default TopNav; -------------------------------------------------------------------------------- /app/components/TopNav/styles.css: -------------------------------------------------------------------------------- 1 | .topnav { 2 | position: fixed; 3 | bottom: 30px; 4 | left: 30px; 5 | } 6 | 7 | .topnav_button { 8 | font-family: sans-serif; 9 | position: absolute; 10 | bottom: 0; 11 | left: 0; 12 | display: inline-block; 13 | width: 48px; 14 | height: 48px; 15 | border-radius: 50%; 16 | border: none; 17 | text-align: center; 18 | line-height: 46px; 19 | font-size: 1.5em; 20 | color: #fff; 21 | z-index: 9999; 22 | background-color: #3ba2e0; 23 | overflow: hidden; 24 | transform: rotate(0deg); 25 | user-select: none; 26 | outline: none; 27 | cursor: pointer; 28 | 29 | transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 30 | transition-delay: 0.2s; 31 | } 32 | 33 | .topnav_button__active { 34 | transform: rotate(-45deg); 35 | transition-delay: 0s; 36 | } 37 | 38 | .topnav_links { 39 | position:absolute; 40 | bottom: 36px; 41 | left: 36px; 42 | display: inline-block; 43 | width: 150px; 44 | z-index: 9998; 45 | transform: translate3d(-50%,50%,0) scale(0); 46 | background-color: #fff; 47 | box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2); 48 | transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); 49 | } 50 | 51 | .topnav_links__active { 52 | transform: translate3d(0%,0%,0) scale(1); 53 | } 54 | 55 | .topnav_link { 56 | position: relative; 57 | display: block; 58 | padding: 5px 10px; 59 | line-height: 32px; 60 | cursor: pointer; 61 | color: #3ba2e0; 62 | text-decoration: none; 63 | font-size: 0.9em; 64 | } 65 | 66 | .topnav_link:hover { 67 | background-color: darken(#fff, 5%); 68 | } -------------------------------------------------------------------------------- /app/components/VerticalAlign/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const VerticalAlign = (props) =>
; 5 | 6 | module.exports = VerticalAlign; -------------------------------------------------------------------------------- /app/components/VerticalAlign/styles.css: -------------------------------------------------------------------------------- 1 | .vert { 2 | position: absolute; 3 | top: 50%; 4 | left: 50%; 5 | 6 | transform: translate(-50%, -50%); 7 | } -------------------------------------------------------------------------------- /app/components/Wrap/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './styles.css'; 4 | 5 | const Button = (props) => { 6 | return ( 7 |
8 |
9 | ); 10 | } 11 | 12 | export default Button; -------------------------------------------------------------------------------- /app/components/Wrap/styles.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | max-width: 700px; 3 | width: 100%; 4 | margin: 0 auto; 5 | } -------------------------------------------------------------------------------- /app/components/ZeroState/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './styles.css'; 3 | 4 | const ZeroState = (props) => { 5 | return ( 6 |
7 |
:(
8 | {props.children} 9 |
10 | ) 11 | } 12 | 13 | export default ZeroState; -------------------------------------------------------------------------------- /app/components/ZeroState/styles.css: -------------------------------------------------------------------------------- 1 | .zerostate { 2 | text-align: center; 3 | padding: 15px 0; 4 | margin-top: 50px; 5 | } 6 | 7 | .zerostate_header { 8 | font-size: 4.8rem; 9 | color: rgba(0,0,0,0.3); 10 | font-weight: bold; 11 | padding: 15px 0; 12 | } 13 | -------------------------------------------------------------------------------- /app/containers/About/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TopNav from '../../components/TopNav'; 3 | import A from '../../components/A'; 4 | import Wrap from '../../components/Wrap'; 5 | 6 | const About = () => { 7 | return ( 8 |
9 | 10 | 11 | H1 12 | 13 |
14 | ); 15 | } 16 | 17 | export default About; -------------------------------------------------------------------------------- /app/containers/About/styles.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/app/containers/About/styles.css -------------------------------------------------------------------------------- /app/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import Helmet from 'react-helmet'; 3 | 4 | export default class App extends Component { 5 | static propTypes = { 6 | children: PropTypes.element.isRequired 7 | }; 8 | 9 | render() { 10 | return ( 11 |
12 | 13 | {this.props.children} 14 |
15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/containers/DevTools.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createDevTools } from 'redux-devtools'; 3 | import LogMonitor from 'redux-devtools-log-monitor'; 4 | import DockMonitor from 'redux-devtools-dock-monitor'; 5 | 6 | export default createDevTools( 7 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /app/containers/HomePage/index.js: -------------------------------------------------------------------------------- 1 | import { bindActionCreators } from 'redux'; 2 | import { connect } from 'react-redux'; 3 | import React, { Component } from 'react'; 4 | import Home from '../../components/Home'; 5 | import * as UserActions from '../../actions/user'; 6 | import styles from './styles.css'; 7 | import Button from '../../components/Button'; 8 | import Posts from '../../components/Posts'; 9 | import getPosts from '../../utils/get_posts.js'; 10 | import VerticalAlign from '../../components/VerticalAlign'; 11 | import Wrap from '../../components/Wrap'; 12 | import TopNav from '../../components/TopNav'; 13 | import H1 from '../../components/H1'; 14 | import P from '../../components/P'; 15 | 16 | class HomePage extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.state = { 21 | loading: true, 22 | posts: [] 23 | } 24 | } 25 | componentDidMount() { 26 | } 27 | render() { 28 | console.log(this.props); 29 | if (JSON.stringify(this.props.user) === '{}') { 30 | return ( 31 | 32 | 33 |

Welcome!

34 |

s'more is a desktop editor for Medium, though you know that already because you're seeing this screen.

35 | 36 |
37 |
38 | ); 39 | } 40 | if (this.state.loading) getPosts(this.props.user.username, (data) => this.setState({loading: false, posts: data.payload.references.Post})); 41 | return ( 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | function mapStateToProps(state) { 51 | return { 52 | user: state.user 53 | } 54 | } 55 | 56 | function mapDispatchToProps(dispatch) { 57 | return bindActionCreators(UserActions, dispatch); 58 | } 59 | 60 | export default connect(mapStateToProps, mapDispatchToProps)(HomePage); -------------------------------------------------------------------------------- /app/containers/HomePage/styles.css: -------------------------------------------------------------------------------- 1 | .wrap { 2 | text-align: center; 3 | 4 | position: absolute; 5 | top: 50%; 6 | left: 50%; 7 | transform: translate(-50%, -50%); 8 | } -------------------------------------------------------------------------------- /app/containers/New/index.js: -------------------------------------------------------------------------------- 1 | // TODO: This needs to be cleaned up 2 | 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import Router from 'react-router'; 6 | import React, { Component } from 'react'; 7 | import Helmet from "react-helmet"; 8 | import * as UserActions from '../../actions/user'; 9 | import Wrap from '../../components/Wrap'; 10 | import A from '../../components/A'; 11 | import TopNav from '../../components/TopNav'; 12 | import Modal from '../../components/Modal'; 13 | import Button from '../../components/Button'; 14 | import H1 from '../../components/H1'; 15 | import P from '../../components/P'; 16 | import Select from '../../components/Select'; 17 | import TagInput from '../../components/TagInput'; 18 | import 'draft-js-emoji-plugin/lib/plugin.css'; 19 | import styles from './styles.css'; 20 | 21 | import create_post from '../../utils/create_post.js'; 22 | 23 | 24 | // load theme styles with webpack 25 | import {stateToHTML} from 'draft-js-export-html'; 26 | import Editor, {RichUtils, createEditorStateWithText} from 'draft-js-plugins-editor'; 27 | import createCounterPlugin from 'draft-js-counter-plugin'; 28 | 29 | const counterPlugin = createCounterPlugin(); 30 | 31 | // Extract a counter from the plugin. 32 | const { WordCounter } = counterPlugin; 33 | 34 | import createRichButtonsPlugin from 'draft-js-richbuttons-plugin'; 35 | const richButtonsPlugin = createRichButtonsPlugin(); 36 | 37 | const { 38 | // inline buttons 39 | ItalicButton, BoldButton, MonospaceButton, UnderlineButton, 40 | // block buttons 41 | ParagraphButton, H1Button, H2Button, ULButton, OLButton 42 | } = richButtonsPlugin; 43 | 44 | const plugins = [counterPlugin, richButtonsPlugin]; 45 | 46 | const StyleButton = ({iconName, toggleInlineStyle, isActive, label, inlineStyle, onMouseDown }) => 47 | 48 | 51 | {iconName} 52 | 53 | ; 54 | 55 | const BlockButton = ({iconName, toggleBlockType, isActive, label, blockType, onMouseDown }) => 56 | 57 | 60 | {iconName} 61 | 62 | ; 63 | 64 | class New extends Component { 65 | constructor(props) { 66 | super(props); 67 | 68 | this.state = { 69 | editorState: createEditorStateWithText(''), 70 | title: '', 71 | fixed: false, 72 | show_modal: false 73 | }; 74 | 75 | this.focus = () => this.refs.editor.focus(); 76 | this.onChange = (editorState) => this.setState({ editorState }); 77 | } 78 | 79 | componentDidMount() { 80 | this.focus(); 81 | } 82 | 83 | render() { 84 | const {editorState} = this.state; 85 | 86 | // If the user changes block type before entering any text, we can 87 | // either style the placeholder or hide it. Let's just hide it now. 88 | let className = 'RichEditor-editor'; 89 | var contentState = editorState.getCurrentContent(); 90 | if (!contentState.hasText()) { 91 | if (contentState.getBlockMap().first().getType() !== 'unstyled') { 92 | className += ' RichEditor-hidePlaceholder'; 93 | } 94 | } 95 | console.log(this.context, this.props); 96 | 97 | return ( 98 | 99 | 100 | 101 | this.setState({ title: e.target.value }) } /> 102 |
103 | 111 |
112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 | words 124 | 125 |
126 |
127 |
128 | this.setState({show_modal: false})}> 129 |

Publish

130 |

Once you publish this story you will not be able to edit in s'more. If you want to add images and links, save this story as a draft, and edit the story in your browser.

131 |

Publish as a 132 | 138 | story. 139 |

140 |
141 |

Tags:

142 | this.setState({tags: tags})} /> 143 |
144 | 149 |
150 |
151 | ); 152 | } 153 | } 154 | 155 | function mapStateToProps(state) { 156 | return { 157 | user: state.user 158 | } 159 | } 160 | 161 | function mapDispatchToProps(dispatch) { 162 | return bindActionCreators(UserActions, dispatch); 163 | } 164 | 165 | export default connect(mapStateToProps, mapDispatchToProps)(New); -------------------------------------------------------------------------------- /app/containers/New/styles.css: -------------------------------------------------------------------------------- 1 | .titleInput { 2 | width: 100%; 3 | display: block; 4 | 5 | color: rgba(0,0,0,.8); 6 | font-family: "Lucida Grande","Lucida Sans Unicode","Lucida Sans",Geneva,Arial,sans-serif; 7 | font-weight: 700; 8 | font-style: normal; 9 | font-size: 36px; 10 | padding: 30px 0; 11 | padding-bottom: 0px; 12 | margin-top: 50px; 13 | margin-left: -2.25px; 14 | line-height: 1.15; 15 | letter-spacing: -.02em; 16 | outline: none; 17 | 18 | border: none; 19 | } 20 | 21 | .controls_wrap { 22 | line-height: 50px; 23 | background-color: white; 24 | 25 | position: fixed; 26 | top: 0; 27 | left: 50%; 28 | transform: translateX(-50%); 29 | width: 100vw; 30 | border-bottom: 1px solid rgba(0,0,0,.8); 31 | } 32 | 33 | .controls { 34 | flex: 1; 35 | display: flex; 36 | max-width: 50%; 37 | } 38 | 39 | .controls_button { 40 | flex: 1; 41 | padding: 0 10px; 42 | display: inline-block; 43 | text-align: center; 44 | cursor: pointer; 45 | } 46 | 47 | .controls_button:hover { 48 | background-color: #efefef; 49 | } 50 | 51 | .controls_button--active { 52 | background-color: #eee; 53 | } 54 | 55 | .controls_button--h1, .controls_button--h2 { 56 | max-width: 30px; 57 | } 58 | 59 | .controls_wrap_action__left, .controls_wrap_action__right { 60 | position: fixed; 61 | top: 0; 62 | } 63 | 64 | .controls_wrap_action__left { 65 | left: 20px; 66 | } 67 | 68 | .controls_wrap_action__right { 69 | right: 20px; 70 | } -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { Provider } from 'react-redux'; 4 | import { Router, hashHistory } from 'react-router'; 5 | import { syncHistoryWithStore } from 'react-router-redux'; 6 | import routes from './routes'; 7 | import configureStore from './store/configureStore'; 8 | import './app.global.css'; 9 | import './app.rich-editor.css'; 10 | 11 | require('dotenv').config(); 12 | 13 | import * as UserActions from './actions/user'; 14 | import get from './utils/get_user'; 15 | import {get_settings} from './utils/settings'; 16 | 17 | const store = configureStore(); 18 | const history = syncHistoryWithStore(hashHistory, store); 19 | 20 | render( 21 | 22 | 23 | , 24 | document.getElementById('root') 25 | ); 26 | get_settings((settings) => { 27 | if (JSON.stringify(settings.user) !== '{}') { 28 | console.log(settings); 29 | store.dispatch(UserActions.set(settings.user)) 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer as routing } from 'react-router-redux'; 3 | import user from './user'; 4 | 5 | const rootReducer = combineReducers({ 6 | user, 7 | routing 8 | }); 9 | 10 | export default rootReducer; 11 | -------------------------------------------------------------------------------- /app/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { SET_USER } from '../actions/user'; 2 | 3 | export default function user(state = {}, action) { 4 | switch (action.type) { 5 | case SET_USER: 6 | return state = action.user; 7 | default: 8 | return state 9 | } 10 | } -------------------------------------------------------------------------------- /app/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, IndexRoute } from 'react-router'; 3 | import App from './containers/App'; 4 | import HomePage from './containers/HomePage'; 5 | import New from './containers/New'; 6 | import About from './containers/About'; 7 | 8 | export default ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | -------------------------------------------------------------------------------- /app/store/configureStore.development.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import { persistState } from 'redux-devtools'; 3 | import thunk from 'redux-thunk'; 4 | import createLogger from 'redux-logger'; 5 | import { hashHistory } from 'react-router'; 6 | import { routerMiddleware } from 'react-router-redux'; 7 | import rootReducer from '../reducers'; 8 | import DevTools from '../containers/DevTools'; 9 | 10 | const logger = createLogger({ 11 | level: 'info', 12 | collapsed: true, 13 | }); 14 | 15 | const router = routerMiddleware(hashHistory); 16 | 17 | const enhancer = compose( 18 | applyMiddleware(thunk, router, logger), 19 | DevTools.instrument(), 20 | persistState( 21 | window.location.href.match( 22 | /[?&]debug_session=([^&]+)\b/ 23 | ) 24 | ) 25 | ); 26 | 27 | export default function configureStore(initialState) { 28 | const store = createStore(rootReducer, initialState, enhancer); 29 | 30 | if (module.hot) { 31 | module.hot.accept('../reducers', () => 32 | store.replaceReducer(require('../reducers')) // eslint-disable-line global-require 33 | ); 34 | } 35 | 36 | return store; 37 | } 38 | -------------------------------------------------------------------------------- /app/store/configureStore.js: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV === 'production') { 2 | module.exports = require('./configureStore.production'); // eslint-disable-line global-require 3 | } else { 4 | module.exports = require('./configureStore.development'); // eslint-disable-line global-require 5 | } 6 | -------------------------------------------------------------------------------- /app/store/configureStore.production.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import { hashHistory } from 'react-router'; 4 | import { routerMiddleware } from 'react-router-redux'; 5 | import rootReducer from '../reducers'; 6 | 7 | const router = routerMiddleware(hashHistory); 8 | 9 | const enhancer = applyMiddleware(thunk, router); 10 | 11 | export default function configureStore(initialState) { 12 | return createStore(rootReducer, initialState, enhancer); 13 | } 14 | -------------------------------------------------------------------------------- /app/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/app/utils/.gitkeep -------------------------------------------------------------------------------- /app/utils/create_post.js: -------------------------------------------------------------------------------- 1 | const medium = require('medium-sdk'); 2 | import * as UserActions from '../actions/user'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import {get_settings} from './settings'; 6 | 7 | var client = new medium.MediumClient({ 8 | clientId: 'b126f4dc1ee3', 9 | clientSecret: '239a493502aa37b1a50f21160a085a7415edd9ca' 10 | }); 11 | 12 | export default function create_post(options, cb) { 13 | get_settings((settings) => { 14 | let user = settings.user; 15 | console.log(options); 16 | client.setAccessToken(user.token.access_token).createPost({ 17 | userId: user.id, 18 | title: options.title, 19 | contentFormat: medium.PostContentFormat.HTML, 20 | content: options.html, 21 | publishStatus: medium.PostPublishStatus[options.status], 22 | tags: options.tags 23 | }, function (err, post) { 24 | console.log(err, post); 25 | cb(post); 26 | }); 27 | }); 28 | } -------------------------------------------------------------------------------- /app/utils/get_posts.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | 3 | export default function getPosts(username, cb) { 4 | console.log(username); 5 | fetch(`https://medium.com/@${username}/latest?format=json`) 6 | .then((response) => { 7 | return response.text(); 8 | }).then((text) => { 9 | text = text.replace('])}while(1);', ''); 10 | return cb(JSON.parse(text)); 11 | }); 12 | } -------------------------------------------------------------------------------- /app/utils/get_user.js: -------------------------------------------------------------------------------- 1 | const {BrowserWindow} = require('electron').remote 2 | const medium = require('medium-sdk'); 3 | import * as UserActions from '../actions/user'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import {get_settings, set_settings} from './settings.js'; 7 | 8 | console.log(global.process.env) 9 | 10 | var client = new medium.MediumClient({ 11 | clientId: 'b126f4dc1ee3', 12 | clientSecret: '239a493502aa37b1a50f21160a085a7415edd9ca' 13 | }); 14 | 15 | const redirectURL = 'http://www.gavin.codes'; 16 | let win = new BrowserWindow({ width: 800, height: 600, show: false }); 17 | 18 | var url = client.getAuthorizationUrl('secretState', redirectURL, [ 19 | medium.Scope.BASIC_PROFILE, medium.Scope.PUBLISH_POST, medium.Scope.LIST_PUBLICATIONS 20 | ]); 21 | 22 | function handleCallback(url, cb) { 23 | var raw_code = /code=([^&]*)/.exec(url) || null; 24 | var code = (raw_code && raw_code.length > 1) ? raw_code[1] : null; 25 | var error = /\?error=(.+)$/.exec(url); 26 | 27 | if (code || error) { 28 | // Close the browser if code found or error 29 | console.log(code); 30 | win.destroy(); 31 | } 32 | 33 | // If there is a code, proceed to get token from github 34 | if (code) { 35 | client.exchangeAuthorizationCode(code, redirectURL, (err, token) => { 36 | client.getUser((err, user) => { 37 | user.token = token; 38 | get_settings((settings) => { 39 | console.log(settings); 40 | settings.user = user; 41 | set_settings(settings, (err) => { 42 | cb(user); 43 | }); 44 | }); 45 | }); 46 | }); 47 | } else if (error) { 48 | alert('Oops! Something went wrong and we couldn\'t' + 49 | 'log you in using Medium. Please try again.'); 50 | } 51 | } 52 | 53 | export default function get(cb) { 54 | get_settings((settings) => { 55 | if (JSON.stringify(settings.user) !== '{}' && new Date() < new Date(settings.user.token.expires_at)) { 56 | console.log('quick', settings); 57 | win.close(); 58 | return cb(settings.user); 59 | } 60 | 61 | win.on('closed', () => { 62 | win = null 63 | }); 64 | 65 | win.webContents.on('will-navigate', (event, url) => { 66 | handleCallback(url, (user) => cb(user)); 67 | }); 68 | 69 | win.webContents.on('did-get-redirect-request', (event, oldUrl, newUrl) => { 70 | handleCallback(newUrl, (user) => cb(user)); 71 | }); 72 | 73 | win.loadURL(url); 74 | win.show(); 75 | }); 76 | } -------------------------------------------------------------------------------- /app/utils/settings.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | export function get_settings(cb) { 5 | fs.readFile(path.join(global.__dirname, 'settings.json'), (err, file) => { 6 | let settings = JSON.parse(file.toString()); 7 | cb(settings); 8 | }); 9 | } 10 | 11 | export function set_settings(settings, cb) { 12 | fs.writeFile(path.join(global.__dirname, 'settings.json'), JSON.stringify(settings), (err) => { 13 | cb(err); 14 | }); 15 | } -------------------------------------------------------------------------------- /erb-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/erb-logo.png -------------------------------------------------------------------------------- /icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/icon.icns -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/icon.ico -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dinubs/smore/434506a9fb83cae7bfdd8a9ddd3da83d17c71cd5/icon.png -------------------------------------------------------------------------------- /main.development.js: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, Menu, shell } from 'electron'; 2 | 3 | let menu; 4 | let template; 5 | let mainWindow = null; 6 | 7 | if (process.env.NODE_ENV === 'development') { 8 | require('electron-debug')(); // eslint-disable-line global-require 9 | } 10 | 11 | 12 | app.on('window-all-closed', () => { 13 | if (process.platform !== 'darwin') app.quit(); 14 | }); 15 | 16 | 17 | app.on('ready', () => { 18 | mainWindow = new BrowserWindow({ 19 | show: false, 20 | width: 1280, 21 | height: 728 22 | }); 23 | 24 | mainWindow.loadURL(`file://${__dirname}/app/app.html`); 25 | 26 | mainWindow.webContents.on('did-finish-load', () => { 27 | mainWindow.show(); 28 | mainWindow.focus(); 29 | }); 30 | 31 | mainWindow.on('closed', () => { 32 | mainWindow = null; 33 | }); 34 | 35 | if (process.env.NODE_ENV === 'development') { 36 | mainWindow.openDevTools(); 37 | mainWindow.webContents.on('context-menu', (e, props) => { 38 | const { x, y } = props; 39 | 40 | Menu.buildFromTemplate([{ 41 | label: 'Inspect element', 42 | click() { 43 | mainWindow.inspectElement(x, y); 44 | } 45 | }]).popup(mainWindow); 46 | }); 47 | } 48 | 49 | if (process.platform === 'darwin') { 50 | template = [{ 51 | label: 'Electron', 52 | submenu: [{ 53 | label: 'About ElectronReact', 54 | selector: 'orderFrontStandardAboutPanel:' 55 | }, { 56 | type: 'separator' 57 | }, { 58 | label: 'Services', 59 | submenu: [] 60 | }, { 61 | type: 'separator' 62 | }, { 63 | label: 'Hide ElectronReact', 64 | accelerator: 'Command+H', 65 | selector: 'hide:' 66 | }, { 67 | label: 'Hide Others', 68 | accelerator: 'Command+Shift+H', 69 | selector: 'hideOtherApplications:' 70 | }, { 71 | label: 'Show All', 72 | selector: 'unhideAllApplications:' 73 | }, { 74 | type: 'separator' 75 | }, { 76 | label: 'Quit', 77 | accelerator: 'Command+Q', 78 | click() { 79 | app.quit(); 80 | } 81 | }] 82 | }, { 83 | label: 'Edit', 84 | submenu: [{ 85 | label: 'Undo', 86 | accelerator: 'Command+Z', 87 | selector: 'undo:' 88 | }, { 89 | label: 'Redo', 90 | accelerator: 'Shift+Command+Z', 91 | selector: 'redo:' 92 | }, { 93 | type: 'separator' 94 | }, { 95 | label: 'Cut', 96 | accelerator: 'Command+X', 97 | selector: 'cut:' 98 | }, { 99 | label: 'Copy', 100 | accelerator: 'Command+C', 101 | selector: 'copy:' 102 | }, { 103 | label: 'Paste', 104 | accelerator: 'Command+V', 105 | selector: 'paste:' 106 | }, { 107 | label: 'Select All', 108 | accelerator: 'Command+A', 109 | selector: 'selectAll:' 110 | }] 111 | }, { 112 | label: 'View', 113 | submenu: (process.env.NODE_ENV === 'development') ? [{ 114 | label: 'Reload', 115 | accelerator: 'Command+R', 116 | click() { 117 | mainWindow.webContents.reload(); 118 | } 119 | }, { 120 | label: 'Toggle Full Screen', 121 | accelerator: 'Ctrl+Command+F', 122 | click() { 123 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 124 | } 125 | }, { 126 | label: 'Toggle Developer Tools', 127 | accelerator: 'Alt+Command+I', 128 | click() { 129 | mainWindow.toggleDevTools(); 130 | } 131 | }] : [{ 132 | label: 'Toggle Full Screen', 133 | accelerator: 'Ctrl+Command+F', 134 | click() { 135 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 136 | } 137 | }] 138 | }, { 139 | label: 'Window', 140 | submenu: [{ 141 | label: 'Minimize', 142 | accelerator: 'Command+M', 143 | selector: 'performMiniaturize:' 144 | }, { 145 | label: 'Close', 146 | accelerator: 'Command+W', 147 | selector: 'performClose:' 148 | }, { 149 | type: 'separator' 150 | }, { 151 | label: 'Bring All to Front', 152 | selector: 'arrangeInFront:' 153 | }] 154 | }, { 155 | label: 'Help', 156 | submenu: [{ 157 | label: 'Learn More', 158 | click() { 159 | shell.openExternal('http://electron.atom.io'); 160 | } 161 | }, { 162 | label: 'Documentation', 163 | click() { 164 | shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); 165 | } 166 | }, { 167 | label: 'Community Discussions', 168 | click() { 169 | shell.openExternal('https://discuss.atom.io/c/electron'); 170 | } 171 | }, { 172 | label: 'Search Issues', 173 | click() { 174 | shell.openExternal('https://github.com/atom/electron/issues'); 175 | } 176 | }] 177 | }]; 178 | 179 | menu = Menu.buildFromTemplate(template); 180 | Menu.setApplicationMenu(menu); 181 | } else { 182 | template = [{ 183 | label: '&File', 184 | submenu: [{ 185 | label: '&Open', 186 | accelerator: 'Ctrl+O' 187 | }, { 188 | label: '&Close', 189 | accelerator: 'Ctrl+W', 190 | click() { 191 | mainWindow.close(); 192 | } 193 | }] 194 | }, { 195 | label: '&View', 196 | submenu: (process.env.NODE_ENV === 'development') ? [{ 197 | label: '&Reload', 198 | accelerator: 'Ctrl+R', 199 | click() { 200 | mainWindow.webContents.reload(); 201 | } 202 | }, { 203 | label: 'Toggle &Full Screen', 204 | accelerator: 'F11', 205 | click() { 206 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 207 | } 208 | }, { 209 | label: 'Toggle &Developer Tools', 210 | accelerator: 'Alt+Ctrl+I', 211 | click() { 212 | mainWindow.toggleDevTools(); 213 | } 214 | }] : [{ 215 | label: 'Toggle &Full Screen', 216 | accelerator: 'F11', 217 | click() { 218 | mainWindow.setFullScreen(!mainWindow.isFullScreen()); 219 | } 220 | }] 221 | }, { 222 | label: 'Help', 223 | submenu: [{ 224 | label: 'Learn More', 225 | click() { 226 | shell.openExternal('http://electron.atom.io'); 227 | } 228 | }, { 229 | label: 'Documentation', 230 | click() { 231 | shell.openExternal('https://github.com/atom/electron/tree/master/docs#readme'); 232 | } 233 | }, { 234 | label: 'Community Discussions', 235 | click() { 236 | shell.openExternal('https://discuss.atom.io/c/electron'); 237 | } 238 | }, { 239 | label: 'Search Issues', 240 | click() { 241 | shell.openExternal('https://github.com/atom/electron/issues'); 242 | } 243 | }] 244 | }]; 245 | menu = Menu.buildFromTemplate(template); 246 | mainWindow.setMenu(menu); 247 | } 248 | }); 249 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0, no-shadow: 0, no-unused-vars: 0, no-console: 0 */ 2 | 'use strict'; 3 | 4 | require('babel-polyfill'); 5 | const os = require('os'); 6 | const webpack = require('webpack'); 7 | const electronCfg = require('./webpack.config.electron.js'); 8 | const cfg = require('./webpack.config.production.js'); 9 | const packager = require('electron-packager'); 10 | const del = require('del'); 11 | const exec = require('child_process').exec; 12 | const argv = require('minimist')(process.argv.slice(2)); 13 | const pkg = require('./package.json'); 14 | const deps = Object.keys(pkg.dependencies); 15 | const devDeps = Object.keys(pkg.devDependencies); 16 | 17 | const appName = argv.name || argv.n || pkg.productName; 18 | const shouldUseAsar = argv.asar || argv.a || false; 19 | const shouldBuildAll = argv.all || false; 20 | 21 | 22 | const DEFAULT_OPTS = { 23 | dir: './', 24 | name: appName, 25 | asar: shouldUseAsar, 26 | ignore: [ 27 | '^/test($|/)', 28 | '^/tools($|/)', 29 | '^/release($|/)', 30 | '^/main.development.js' 31 | ].concat(devDeps.map(name => `/node_modules/${name}($|/)`)) 32 | .concat( 33 | deps.filter(name => !electronCfg.externals.includes(name)) 34 | .map(name => `/node_modules/${name}($|/)`) 35 | ) 36 | }; 37 | 38 | const icon = argv.icon || argv.i || 'app/app'; 39 | 40 | if (icon) { 41 | DEFAULT_OPTS.icon = icon; 42 | } 43 | 44 | const version = argv.version || argv.v; 45 | 46 | if (version) { 47 | DEFAULT_OPTS.version = version; 48 | startPack(); 49 | } else { 50 | // use the same version as the currently-installed electron-prebuilt 51 | exec('npm list electron-prebuilt --dev', (err, stdout) => { 52 | if (err) { 53 | DEFAULT_OPTS.version = '1.2.0'; 54 | } else { 55 | DEFAULT_OPTS.version = stdout.split('electron-prebuilt@')[1].replace(/\s/g, ''); 56 | } 57 | 58 | startPack(); 59 | }); 60 | } 61 | 62 | 63 | function build(cfg) { 64 | return new Promise((resolve, reject) => { 65 | webpack(cfg, (err, stats) => { 66 | if (err) return reject(err); 67 | resolve(stats); 68 | }); 69 | }); 70 | } 71 | 72 | function startPack() { 73 | console.log('start pack...'); 74 | build(electronCfg) 75 | .then(() => build(cfg)) 76 | .then(() => del('release')) 77 | .then(paths => { 78 | if (shouldBuildAll) { 79 | // build for all platforms 80 | const archs = ['ia32', 'x64']; 81 | const platforms = ['linux', 'win32', 'darwin']; 82 | 83 | platforms.forEach(plat => { 84 | archs.forEach(arch => { 85 | pack(plat, arch, log(plat, arch)); 86 | }); 87 | }); 88 | } else { 89 | // build for current platform only 90 | pack(os.platform(), os.arch(), log(os.platform(), os.arch())); 91 | } 92 | }) 93 | .catch(err => { 94 | console.error(err); 95 | }); 96 | } 97 | 98 | function pack(plat, arch, cb) { 99 | // there is no darwin ia32 electron 100 | if (plat === 'darwin' && arch === 'ia32') return; 101 | 102 | const iconObj = { 103 | icon: DEFAULT_OPTS.icon + (() => { 104 | let extension = '.png'; 105 | if (plat === 'darwin') { 106 | extension = '.icns'; 107 | } else if (plat === 'win32') { 108 | extension = '.ico'; 109 | } 110 | return extension; 111 | })() 112 | }; 113 | 114 | const opts = Object.assign({}, DEFAULT_OPTS, iconObj, { 115 | platform: plat, 116 | arch, 117 | prune: true, 118 | 'app-version': pkg.version || DEFAULT_OPTS.version, 119 | out: `release/${plat}-${arch}` 120 | }); 121 | 122 | packager(opts, cb); 123 | } 124 | 125 | 126 | function log(plat, arch) { 127 | return (err, filepath) => { 128 | if (err) return console.error(err); 129 | console.log(`${plat}-${arch} finished!`); 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-react-boilerplate", 3 | "productName": "ElectronReact", 4 | "version": "0.10.0", 5 | "description": "Electron application boilerplate based on React, React Router, Webpack, React Hot Loader for rapid application development", 6 | "main": "main.js", 7 | "scripts": { 8 | "test": "cross-env NODE_ENV=test mocha --compilers js:babel-register --recursive --require ./test/setup.js test/**/*.spec.js", 9 | "test-watch": "npm test -- --watch", 10 | "test-e2e": "cross-env NODE_ENV=test mocha --compilers js:babel-register --require ./test/setup.js --require co-mocha ./test/e2e.js", 11 | "lint": "eslint app test *.js", 12 | "hot-server": "node -r babel-register server.js", 13 | "build-main": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.electron.js --progress --profile --colors", 14 | "build-renderer": "cross-env NODE_ENV=production node -r babel-register ./node_modules/webpack/bin/webpack --config webpack.config.production.js --progress --profile --colors", 15 | "build": "npm run build-main && npm run build-renderer", 16 | "start": "cross-env NODE_ENV=production electron ./", 17 | "start-hot": "cross-env HOT=1 NODE_ENV=development electron -r babel-register ./main.development", 18 | "package": "cross-env NODE_ENV=production node -r babel-register package.js", 19 | "package-all": "npm run package -- --all", 20 | "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json", 21 | "dev": "concurrently --kill-others \"npm run hot-server\" \"npm run start-hot\"" 22 | }, 23 | "bin": { 24 | "electron": "./node_modules/.bin/electron" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/chentsulin/electron-react-boilerplate.git" 29 | }, 30 | "author": { 31 | "name": "C. T. Lin", 32 | "email": "chentsulin@gmail.com", 33 | "url": "https://github.com/chentsulin" 34 | }, 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/chentsulin/electron-react-boilerplate/issues" 38 | }, 39 | "keywords": [ 40 | "electron", 41 | "boilerplate", 42 | "react", 43 | "react-router", 44 | "flux", 45 | "webpack", 46 | "react-hot" 47 | ], 48 | "homepage": "https://github.com/chentsulin/electron-react-boilerplate#readme", 49 | "devDependencies": { 50 | "asar": "^0.11.0", 51 | "babel-core": "^6.9.0", 52 | "babel-eslint": "^6.0.4", 53 | "babel-loader": "^6.2.4", 54 | "babel-plugin-add-module-exports": "^0.2.1", 55 | "babel-plugin-dev-expression": "^0.2.1", 56 | "babel-plugin-transform-remove-console": "^6.8.0", 57 | "babel-plugin-transform-remove-debugger": "^6.8.0", 58 | "babel-plugin-webpack-loaders": "^0.5.0", 59 | "babel-polyfill": "^6.9.0", 60 | "babel-preset-es2015": "^6.9.0", 61 | "babel-preset-react": "^6.5.0", 62 | "babel-preset-react-hmre": "^1.1.1", 63 | "babel-preset-react-optimize": "^1.0.1", 64 | "babel-preset-stage-0": "^6.5.0", 65 | "babel-register": "^6.9.0", 66 | "chai": "^3.5.0", 67 | "chromedriver": "^2.21.2", 68 | "co-mocha": "^1.1.2", 69 | "concurrently": "^2.1.0", 70 | "cross-env": "^1.0.8", 71 | "css-loader": "^0.23.1", 72 | "del": "^2.2.0", 73 | "devtron": "^1.2.0", 74 | "electron-devtools-installer": "^1.1.2", 75 | "electron-packager": "^7.0.2", 76 | "electron-prebuilt": "^1.2.3", 77 | "electron-rebuild": "^1.1.4", 78 | "eslint": "^2.10.2", 79 | "eslint-config-airbnb": "^9.0.1", 80 | "eslint-import-resolver-webpack": "^0.3.0", 81 | "eslint-plugin-import": "^1.8.1", 82 | "eslint-plugin-jsx-a11y": "^1.2.2", 83 | "eslint-plugin-react": "^5.1.1", 84 | "express": "^4.13.4", 85 | "extract-text-webpack-plugin": "^1.0.1", 86 | "fbjs-scripts": "^0.7.1", 87 | "jsdom": "^9.2.0", 88 | "json-loader": "^0.5.4", 89 | "minimist": "^1.2.0", 90 | "mocha": "^2.5.3", 91 | "node-libs-browser": "^1.0.0", 92 | "react-addons-test-utils": "^15.1.0", 93 | "redux-devtools": "^3.3.1", 94 | "redux-devtools-dock-monitor": "^1.1.1", 95 | "redux-devtools-log-monitor": "^1.0.11", 96 | "redux-logger": "^2.6.1", 97 | "selenium-webdriver": "^2.53.2", 98 | "sinon": "^1.17.4", 99 | "style-loader": "^0.13.1", 100 | "webpack": "^1.13.1", 101 | "webpack-dev-middleware": "^1.6.1", 102 | "webpack-hot-middleware": "^2.10.0" 103 | }, 104 | "dependencies": { 105 | "css-modules-require-hook": "^4.0.0", 106 | "dotenv": "^2.0.0", 107 | "draft-js": "^0.7.0", 108 | "draft-js-counter-plugin": "^1.0.0", 109 | "draft-js-emoji-plugin": "^1.2.2", 110 | "draft-js-export-html": "^0.3.0", 111 | "draft-js-plugins-editor": "^1.1.0", 112 | "draft-js-richbuttons-plugin": "^1.1.0", 113 | "electron-debug": "^1.0.1", 114 | "font-awesome": "^4.6.3", 115 | "medium-sdk": "0.0.3", 116 | "postcss": "^5.0.21", 117 | "react": "^15.1.0", 118 | "react-dom": "^15.1.0", 119 | "react-helmet": "^3.1.0", 120 | "react-redux": "^4.4.5", 121 | "react-router": "^2.4.1", 122 | "react-router-redux": "^4.0.4", 123 | "react-tagsinput": "^3.9.0", 124 | "redux": "^3.5.2", 125 | "redux-thunk": "^2.1.0", 126 | "source-map-support": "^0.4.0", 127 | "whatwg-fetch": "^1.0.0" 128 | }, 129 | "devEngines": { 130 | "node": "4.x || 5.x || 6.x", 131 | "npm": "2.x || 3.x" 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 3 | import express from 'express'; 4 | import webpack from 'webpack'; 5 | import webpackDevMiddleware from 'webpack-dev-middleware'; 6 | import webpackHotMiddleware from 'webpack-hot-middleware'; 7 | 8 | import config from './webpack.config.development'; 9 | 10 | const app = express(); 11 | const compiler = webpack(config); 12 | const PORT = 3000; 13 | 14 | const wdm = webpackDevMiddleware(compiler, { 15 | publicPath: config.output.publicPath, 16 | stats: { 17 | colors: true 18 | } 19 | }); 20 | 21 | app.use(wdm); 22 | 23 | app.use(webpackHotMiddleware(compiler)); 24 | 25 | const server = app.listen(PORT, 'localhost', err => { 26 | if (err) { 27 | console.error(err); 28 | return; 29 | } 30 | 31 | console.log(`Listening at http://localhost:${PORT}`); 32 | }); 33 | 34 | process.on('SIGTERM', () => { 35 | console.log('Stopping dev server'); 36 | wdm.close(); 37 | server.close(() => { 38 | process.exit(0); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/actions/counter.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import { expect } from 'chai'; 3 | import { spy } from 'sinon'; 4 | import * as actions from '../../app/actions/counter'; 5 | 6 | 7 | describe('actions', () => { 8 | it('increment should create increment action', () => { 9 | expect(actions.increment()).to.deep.equal({ type: actions.INCREMENT_COUNTER }); 10 | }); 11 | 12 | it('decrement should create decrement action', () => { 13 | expect(actions.decrement()).to.deep.equal({ type: actions.DECREMENT_COUNTER }); 14 | }); 15 | 16 | it('incrementIfOdd should create increment action', () => { 17 | const fn = actions.incrementIfOdd(); 18 | expect(fn).to.be.a('function'); 19 | const dispatch = spy(); 20 | const getState = () => ({ counter: 1 }); 21 | fn(dispatch, getState); 22 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).to.be.true; 23 | }); 24 | 25 | it('incrementIfOdd shouldnt create increment action if counter is even', () => { 26 | const fn = actions.incrementIfOdd(); 27 | const dispatch = spy(); 28 | const getState = () => ({ counter: 2 }); 29 | fn(dispatch, getState); 30 | expect(dispatch.called).to.be.false; 31 | }); 32 | 33 | // There's no nice way to test this at the moment... 34 | it('incrementAsync', (done) => { 35 | const fn = actions.incrementAsync(1); 36 | expect(fn).to.be.a('function'); 37 | const dispatch = spy(); 38 | fn(dispatch); 39 | setTimeout(() => { 40 | expect(dispatch.calledWith({ type: actions.INCREMENT_COUNTER })).to.be.true; 41 | done(); 42 | }, 5); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /test/components/Counter.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import { expect } from 'chai'; 3 | import { spy } from 'sinon'; 4 | import React from 'react'; 5 | import { 6 | renderIntoDocument, 7 | scryRenderedDOMComponentsWithTag, 8 | findRenderedDOMComponentWithClass, 9 | Simulate 10 | } from 'react-addons-test-utils'; 11 | import Counter from '../../app/components/Counter'; 12 | 13 | 14 | function setup() { 15 | const actions = { 16 | increment: spy(), 17 | incrementIfOdd: spy(), 18 | incrementAsync: spy(), 19 | decrement: spy() 20 | }; 21 | const component = renderIntoDocument(); 22 | return { 23 | component, 24 | actions, 25 | buttons: scryRenderedDOMComponentsWithTag(component, 'button').map(button => button), 26 | p: findRenderedDOMComponentWithClass(component, 'counter') 27 | }; 28 | } 29 | 30 | 31 | describe('Counter component', () => { 32 | it('should display count', () => { 33 | const { p } = setup(); 34 | expect(p.textContent).to.match(/^1$/); 35 | }); 36 | 37 | it('first button should call increment', () => { 38 | const { buttons, actions } = setup(); 39 | Simulate.click(buttons[0]); 40 | expect(actions.increment.called).to.be.true; 41 | }); 42 | 43 | it('second button should call decrement', () => { 44 | const { buttons, actions } = setup(); 45 | Simulate.click(buttons[1]); 46 | expect(actions.decrement.called).to.be.true; 47 | }); 48 | 49 | it('third button should call incrementIfOdd', () => { 50 | const { buttons, actions } = setup(); 51 | Simulate.click(buttons[2]); 52 | expect(actions.incrementIfOdd.called).to.be.true; 53 | }); 54 | 55 | it('fourth button should call incrementAsync', () => { 56 | const { buttons, actions } = setup(); 57 | Simulate.click(buttons[3]); 58 | expect(actions.incrementAsync.called).to.be.true; 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/containers/CounterPage.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import React from 'react'; 3 | import { 4 | renderIntoDocument, 5 | scryRenderedDOMComponentsWithTag, 6 | findRenderedDOMComponentWithClass, 7 | Simulate 8 | } from 'react-addons-test-utils'; 9 | import { Provider } from 'react-redux'; 10 | import CounterPage from '../../app/containers/CounterPage'; 11 | import configureStore from '../../app/store/configureStore'; 12 | 13 | 14 | function setup(initialState) { 15 | const store = configureStore(initialState); 16 | const app = renderIntoDocument( 17 | 18 | 19 | 20 | ); 21 | return { 22 | app, 23 | buttons: scryRenderedDOMComponentsWithTag(app, 'button').map(button => button), 24 | p: findRenderedDOMComponentWithClass(app, 'counter') 25 | }; 26 | } 27 | 28 | 29 | describe('containers', () => { 30 | describe('App', () => { 31 | it('should display initial count', () => { 32 | const { p } = setup(); 33 | expect(p.textContent).to.match(/^0$/); 34 | }); 35 | 36 | it('should display updated count after increment button click', () => { 37 | const { buttons, p } = setup(); 38 | Simulate.click(buttons[0]); 39 | expect(p.textContent).to.match(/^1$/); 40 | }); 41 | 42 | it('should display updated count after descrement button click', () => { 43 | const { buttons, p } = setup(); 44 | Simulate.click(buttons[1]); 45 | expect(p.textContent).to.match(/^-1$/); 46 | }); 47 | 48 | it('shouldnt change if even and if odd button clicked', () => { 49 | const { buttons, p } = setup(); 50 | Simulate.click(buttons[2]); 51 | expect(p.textContent).to.match(/^0$/); 52 | }); 53 | 54 | it('should change if odd and if odd button clicked', () => { 55 | const { buttons, p } = setup({ counter: 1 }); 56 | Simulate.click(buttons[2]); 57 | expect(p.textContent).to.match(/^2$/); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /test/e2e.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import chromedriver from 'chromedriver'; 3 | import webdriver from 'selenium-webdriver'; 4 | import { expect } from 'chai'; 5 | import electronPath from 'electron-prebuilt'; 6 | import homeStyles from '../app/components/Home.css'; 7 | import counterStyles from '../app/components/Counter.css'; 8 | 9 | chromedriver.start(); // on port 9515 10 | process.on('exit', chromedriver.stop); 11 | 12 | const delay = time => new Promise(resolve => setTimeout(resolve, time)); 13 | 14 | describe('main window', function spec() { 15 | this.timeout(5000); 16 | 17 | before(async () => { 18 | await delay(1000); // wait chromedriver start time 19 | this.driver = new webdriver.Builder() 20 | .usingServer('http://localhost:9515') 21 | .withCapabilities({ 22 | chromeOptions: { 23 | binary: electronPath, 24 | args: [`app=${path.resolve()}`] 25 | } 26 | }) 27 | .forBrowser('electron') 28 | .build(); 29 | }); 30 | 31 | after(async () => { 32 | await this.driver.quit(); 33 | }); 34 | 35 | const findCounter = () => this.driver.findElement(webdriver.By.className(counterStyles.counter)); 36 | 37 | const findButtons = () => this.driver.findElements(webdriver.By.className(counterStyles.btn)); 38 | 39 | it('should open window', async () => { 40 | const title = await this.driver.getTitle(); 41 | expect(title).to.equal('Hello Electron React!'); 42 | }); 43 | 44 | it('should to Counter with click "to Counter" link', async () => { 45 | const link = await this.driver.findElement(webdriver.By.css(`.${homeStyles.container} > a`)); 46 | link.click(); 47 | 48 | const counter = await findCounter(); 49 | expect(await counter.getText()).to.equal('0'); 50 | }); 51 | 52 | it('should display updated count after increment button click', async () => { 53 | const buttons = await findButtons(); 54 | buttons[0].click(); 55 | 56 | const counter = await findCounter(); 57 | expect(await counter.getText()).to.equal('1'); 58 | }); 59 | 60 | it('should display updated count after descrement button click', async () => { 61 | const buttons = await findButtons(); 62 | const counter = await findCounter(); 63 | 64 | buttons[1].click(); // - 65 | 66 | expect(await counter.getText()).to.equal('0'); 67 | }); 68 | 69 | it('shouldnt change if even and if odd button clicked', async () => { 70 | const buttons = await findButtons(); 71 | const counter = await findCounter(); 72 | buttons[2].click(); // odd 73 | 74 | expect(await counter.getText()).to.equal('0'); 75 | }); 76 | 77 | it('should change if odd and if odd button clicked', async () => { 78 | const buttons = await findButtons(); 79 | const counter = await findCounter(); 80 | 81 | buttons[0].click(); // + 82 | buttons[2].click(); // odd 83 | 84 | expect(await counter.getText()).to.equal('2'); 85 | }); 86 | 87 | it('should change if async button clicked and a second later', async () => { 88 | const buttons = await findButtons(); 89 | const counter = await findCounter(); 90 | buttons[3].click(); // async 91 | 92 | expect(await counter.getText()).to.equal('2'); 93 | 94 | await this.driver.wait(() => 95 | counter.getText().then(text => text === '3') 96 | , 1000, 'count not as expected'); 97 | }); 98 | 99 | it('should back to home if back button clicked', async () => { 100 | const link = await this.driver.findElement( 101 | webdriver.By.css(`.${counterStyles.backButton} > a`) 102 | ); 103 | link.click(); 104 | 105 | await this.driver.findElement(webdriver.By.className(homeStyles.container)); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /test/example.js: -------------------------------------------------------------------------------- 1 | /* eslint func-names: 0 */ 2 | import { expect } from 'chai'; 3 | 4 | 5 | describe('description', () => { 6 | it('description', () => { 7 | expect(1 + 2).to.equal(3); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/reducers/counter.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import counter from '../../app/reducers/counter'; 3 | import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../../app/actions/counter'; 4 | 5 | 6 | describe('reducers', () => { 7 | describe('counter', () => { 8 | it('should handle initial state', () => { 9 | expect(counter(undefined, {})).to.equal(0); 10 | }); 11 | 12 | it('should handle INCREMENT_COUNTER', () => { 13 | expect(counter(1, { type: INCREMENT_COUNTER })).to.equal(2); 14 | }); 15 | 16 | it('should handle DECREMENT_COUNTER', () => { 17 | expect(counter(1, { type: DECREMENT_COUNTER })).to.equal(0); 18 | }); 19 | 20 | it('should handle unknown action type', () => { 21 | expect(counter(1, { type: 'unknown' })).to.equal(1); 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill'; 2 | import { jsdom } from 'jsdom'; 3 | 4 | global.document = jsdom(''); 5 | global.window = document.defaultView; 6 | global.navigator = global.window.navigator; 7 | window.localStorage = window.sessionStorage = { 8 | getItem(key) { 9 | return this[key]; 10 | }, 11 | setItem(key, value) { 12 | this[key] = value; 13 | }, 14 | removeItem(key) { 15 | this[key] = undefined; 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /webpack.config.base.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | module: { 5 | loaders: [{ 6 | test: /\.jsx?$/, 7 | loaders: ['babel-loader'], 8 | exclude: /node_modules/ 9 | }, { 10 | test: /\.json$/, 11 | loader: 'json-loader' 12 | }] 13 | }, 14 | output: { 15 | path: path.join(__dirname, 'dist'), 16 | filename: 'bundle.js', 17 | libraryTarget: 'commonjs2' 18 | }, 19 | resolve: { 20 | extensions: ['', '.js', '.jsx'], 21 | packageMains: ['webpack', 'browser', 'web', 'browserify', ['jam', 'main'], 'main'] 22 | }, 23 | plugins: [ 24 | 25 | ], 26 | externals: [ 27 | // put your node 3rd party libraries which can't be built with webpack here 28 | // (mysql, mongodb, and so on..) 29 | 'medium-sdk', 30 | 'dotenv' 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /webpack.config.development.js: -------------------------------------------------------------------------------- 1 | /* eslint max-len: 0 */ 2 | import webpack from 'webpack'; 3 | import baseConfig from './webpack.config.base'; 4 | 5 | const config = { 6 | ...baseConfig, 7 | 8 | debug: true, 9 | 10 | devtool: 'cheap-module-eval-source-map', 11 | 12 | entry: [ 13 | 'webpack-hot-middleware/client?path=http://localhost:3000/__webpack_hmr', 14 | './app/index' 15 | ], 16 | 17 | output: { 18 | ...baseConfig.output, 19 | publicPath: 'http://localhost:3000/dist/' 20 | }, 21 | 22 | module: { 23 | ...baseConfig.module, 24 | loaders: [ 25 | ...baseConfig.module.loaders, 26 | 27 | { 28 | test: /\.global\.css$/, 29 | loaders: [ 30 | 'style-loader', 31 | 'css-loader?sourceMap' 32 | ] 33 | }, 34 | 35 | { 36 | test: /^((?!\.global).)*\.css$/, 37 | loaders: [ 38 | 'style-loader', 39 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 40 | ] 41 | } 42 | ] 43 | }, 44 | 45 | plugins: [ 46 | ...baseConfig.plugins, 47 | new webpack.HotModuleReplacementPlugin(), 48 | new webpack.NoErrorsPlugin(), 49 | new webpack.DefinePlugin({ 50 | 'process.env.NODE_ENV': JSON.stringify('development') 51 | }) 52 | ], 53 | 54 | target: 'electron-renderer' 55 | }; 56 | 57 | export default config; 58 | -------------------------------------------------------------------------------- /webpack.config.electron.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import baseConfig from './webpack.config.base'; 3 | 4 | export default { 5 | ...baseConfig, 6 | 7 | devtool: 'source-map', 8 | 9 | entry: './main.development', 10 | 11 | output: { 12 | ...baseConfig.output, 13 | path: __dirname, 14 | filename: './main.js' 15 | }, 16 | 17 | plugins: [ 18 | new webpack.optimize.UglifyJsPlugin({ 19 | compressor: { 20 | warnings: false 21 | } 22 | }), 23 | new webpack.BannerPlugin( 24 | 'require("source-map-support").install();', 25 | { raw: true, entryOnly: false } 26 | ), 27 | new webpack.DefinePlugin({ 28 | 'process.env': { 29 | NODE_ENV: JSON.stringify('production') 30 | } 31 | }) 32 | ], 33 | 34 | target: 'electron-main', 35 | 36 | node: { 37 | __dirname: false, 38 | __filename: false 39 | }, 40 | 41 | externals: [ 42 | ...baseConfig.externals, 43 | 'font-awesome', 44 | 'source-map-support' 45 | ] 46 | }; 47 | -------------------------------------------------------------------------------- /webpack.config.node.js: -------------------------------------------------------------------------------- 1 | // for babel-plugin-webpack-loaders 2 | require('babel-register'); 3 | const devConfigs = require('./webpack.config.development'); 4 | 5 | module.exports = { 6 | output: { 7 | libraryTarget: 'commonjs2' 8 | }, 9 | module: { 10 | loaders: devConfigs.module.loaders.slice(1) // remove babel-loader 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /webpack.config.production.js: -------------------------------------------------------------------------------- 1 | import webpack from 'webpack'; 2 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 3 | import baseConfig from './webpack.config.base'; 4 | 5 | const config = { 6 | ...baseConfig, 7 | 8 | devtool: 'source-map', 9 | 10 | entry: './app/index', 11 | 12 | output: { 13 | ...baseConfig.output, 14 | 15 | publicPath: '../dist/' 16 | }, 17 | 18 | module: { 19 | ...baseConfig.module, 20 | 21 | loaders: [ 22 | ...baseConfig.module.loaders, 23 | 24 | { 25 | test: /\.global\.css$/, 26 | loader: ExtractTextPlugin.extract( 27 | 'style-loader', 28 | 'css-loader' 29 | ) 30 | }, 31 | 32 | { 33 | test: /^((?!\.global).)*\.css$/, 34 | loader: ExtractTextPlugin.extract( 35 | 'style-loader', 36 | 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]' 37 | ) 38 | } 39 | ] 40 | }, 41 | 42 | plugins: [ 43 | ...baseConfig.plugins, 44 | new webpack.optimize.OccurenceOrderPlugin(), 45 | new webpack.DefinePlugin({ 46 | 'process.env.NODE_ENV': JSON.stringify('production') 47 | }), 48 | new webpack.optimize.UglifyJsPlugin({ 49 | compressor: { 50 | screw_ie8: true, 51 | warnings: false 52 | } 53 | }), 54 | new ExtractTextPlugin('style.css', { allChunks: true }) 55 | ], 56 | 57 | target: 'electron-renderer' 58 | }; 59 | 60 | export default config; 61 | --------------------------------------------------------------------------------