├── .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 |
8 |
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 |
this.toggleNav()}>+
26 |
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 | this.props.getFromMedium()}>Sign In w/ Medium
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 | this.setState({show_modal: true})} style={{display: 'inline-block', marginTop: '3px', marginLeft: '10px', width: 'auto'}}>Publish
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 | {this.setState({status: e.target.value})}}>
133 | choose one
134 | draft
135 | public
136 | unlisted
137 |
138 | story.
139 |
140 |
141 |
Tags:
142 |
this.setState({tags: tags})} />
143 |
144 | {
145 | create_post({title: this.state.title, html: stateToHTML(contentState), tags: this.state.tags, status: this.state.status}, (post) => {
146 | this.props.history.push('/');
147 | });
148 | }}>Publish
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 |
--------------------------------------------------------------------------------